@warlock.js/cache 4.15.0 → 4.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"redis-cache-driver.mjs","names":[],"sources":["../../../../../../../cache/src/drivers/redis-cache-driver.ts"],"sourcesContent":["import { log } from \"@warlock.js/logger\";\r\nimport type { createClient } from \"redis\";\r\nimport type {\r\n CacheData,\r\n CacheDriver,\r\n CacheKey,\r\n CacheSetOptions,\r\n CacheSetResult,\r\n CacheTtl,\r\n RedisOptions,\r\n} from \"../types\";\r\nimport { CacheConfigurationError, CacheUnsupportedError } from \"../types\";\r\nimport { BaseCacheDriver } from \"./base-cache-driver\";\r\n\r\n// ============================================================\r\n// Lazy-loaded Redis SDK Types\r\n// ============================================================\r\n\r\n/**\r\n * Cached Redis module (loaded once, reused)\r\n */\r\nlet RedisClient: typeof import(\"redis\");\r\n\r\nlet isModuleExists: boolean | null = null;\r\n\r\n/**\r\n * Installation instructions for Redis package\r\n */\r\nconst REDIS_INSTALL_INSTRUCTIONS = `\r\nRedis cache driver requires the redis package.\r\nInstall it with:\r\n\r\n npm install redis\r\n\r\nOr with your preferred package manager:\r\n\r\n pnpm add redis\r\n yarn add redis\r\n`.trim();\r\n\r\n/**\r\n * Load Redis module\r\n */\r\nasync function loadRedis() {\r\n try {\r\n RedisClient = await import(\"redis\");\r\n isModuleExists = true;\r\n } catch {\r\n isModuleExists = false;\r\n }\r\n}\r\n\r\nloadRedis();\r\n\r\n// ============================================================\r\n// RedisCacheDriver Class\r\n// ============================================================\r\n\r\nexport class RedisCacheDriver\r\n extends BaseCacheDriver<ReturnType<typeof createClient>, RedisOptions>\r\n implements CacheDriver<ReturnType<typeof createClient>, RedisOptions>\r\n{\r\n /**\r\n * Cache driver name\r\n */\r\n public name = \"redis\";\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public setOptions(options: RedisOptions) {\r\n if (!options.url && !options.host) {\r\n throw new CacheConfigurationError(\r\n \"Redis driver requires either 'url' or 'host' option to be configured.\",\r\n );\r\n }\r\n\r\n return super.setOptions(options);\r\n }\r\n\r\n /**\r\n * {@inheritDoc}\r\n */\r\n public async removeNamespace(namespace: string) {\r\n namespace = this.parseKey(namespace);\r\n\r\n this.log(\"clearing\", namespace);\r\n\r\n const keys = await this.client?.keys(`${namespace}*`);\r\n\r\n if (!keys || keys.length === 0) {\r\n this.log(\"notFound\", namespace);\r\n return;\r\n }\r\n\r\n await this.client?.del(keys);\r\n\r\n this.log(\"cleared\", namespace);\r\n\r\n return keys;\r\n }\r\n\r\n /**\r\n * {@inheritDoc}\r\n */\r\n public async set(\r\n key: CacheKey,\r\n value: any,\r\n ttlOrOptions?: CacheTtl | CacheSetOptions,\r\n ): Promise<any> {\r\n const parsedKey = this.parseKey(key);\r\n const { ttl, tags, onConflict, vector, staleAt } = this.resolveSetOptions(ttlOrOptions);\r\n\r\n if (vector) {\r\n throw new CacheUnsupportedError(\r\n \"'redis' driver does not yet support similarity retrieval. Phase 2 (RediSearch) is on the backlog — use a memory driver or the 'pg' driver (with pgvector) for now.\",\r\n );\r\n }\r\n\r\n this.log(\"caching\", parsedKey);\r\n\r\n const serialized = JSON.stringify(value);\r\n const hasExpiry = Boolean(ttl) && ttl !== Infinity;\r\n\r\n let reply: string | null | undefined;\r\n\r\n if (onConflict === \"create\") {\r\n const options: { NX: true; EX?: number } = { NX: true };\r\n if (hasExpiry) {\r\n options.EX = ttl as number;\r\n }\r\n reply = await this.client?.set(parsedKey, serialized, options);\r\n } else if (onConflict === \"update\") {\r\n const options: { XX: true; EX?: number } = { XX: true };\r\n if (hasExpiry) {\r\n options.EX = ttl as number;\r\n }\r\n reply = await this.client?.set(parsedKey, serialized, options);\r\n } else if (hasExpiry) {\r\n reply = await this.client?.set(parsedKey, serialized, { EX: ttl as number });\r\n } else {\r\n reply = await this.client?.set(parsedKey, serialized);\r\n }\r\n\r\n const wasSet = reply === \"OK\";\r\n\r\n if ((onConflict === \"create\" || onConflict === \"update\") && !wasSet) {\r\n const existing = onConflict === \"create\" ? ((await this.get(key)) as any) : null;\r\n return { wasSet: false, existing } satisfies CacheSetResult;\r\n }\r\n\r\n if (tags && tags.length > 0) {\r\n await this.applyTags(parsedKey, tags);\r\n }\r\n\r\n if (staleAt !== undefined) {\r\n // Sidecar key for SWR freshness — keeps the main value JSON\r\n // backwards-compatible with entries written before SWR landed.\r\n const sidecarOptions: { EX?: number } = {};\r\n\r\n if (hasExpiry) {\r\n sidecarOptions.EX = ttl as number;\r\n }\r\n\r\n await this.client?.set(this.swrMetaKey(parsedKey), String(staleAt), sidecarOptions);\r\n }\r\n\r\n this.log(\"cached\", parsedKey);\r\n\r\n await this.emit(\"set\", { key: parsedKey, value, ttl });\r\n\r\n if (onConflict === \"create\" || onConflict === \"update\") {\r\n return { wasSet: true, existing: null } satisfies CacheSetResult;\r\n }\r\n\r\n return value;\r\n }\r\n\r\n /**\r\n * Build the sidecar key Redis uses to track SWR freshness without\r\n * wrapping the main value JSON.\r\n */\r\n protected swrMetaKey(parsedKey: string): string {\r\n return `__swrmeta:${parsedKey}`;\r\n }\r\n\r\n /**\r\n * Read the raw {@link CacheData} wrapper, fetching the value and the\r\n * SWR sidecar in parallel. Returns `null` when the main key is missing\r\n * or expired (Redis handles expiry natively, so the absence of the\r\n * value alone tells us).\r\n */\r\n protected async getEntry(key: CacheKey): Promise<CacheData | null> {\r\n const parsedKey = this.parseKey(key);\r\n\r\n const [valueRaw, staleAtRaw] = await Promise.all([\r\n this.client?.get(parsedKey),\r\n this.client?.get(this.swrMetaKey(parsedKey)),\r\n ]);\r\n\r\n if (!valueRaw) {\r\n return null;\r\n }\r\n\r\n const data = JSON.parse(valueRaw);\r\n const staleAt = staleAtRaw ? Number(staleAtRaw) : undefined;\r\n\r\n return staleAt !== undefined ? { data, staleAt } : { data };\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n *\r\n * Redis tracks expiry natively (the payload carries no `expiresAt`), so read\r\n * the remaining lifetime with the `TTL` command. Redis returns `-2` for a\r\n * missing key and `-1` for a key with no expiry.\r\n */\r\n protected async getRemainingTtl(key: CacheKey): Promise<number | undefined> {\r\n const parsedKey = this.parseKey(key);\r\n const ttl = await this.client?.ttl(parsedKey);\r\n\r\n if (ttl === undefined || ttl === -2) {\r\n return undefined;\r\n }\r\n\r\n if (ttl === -1) {\r\n return Infinity;\r\n }\r\n\r\n return ttl;\r\n }\r\n\r\n /**\r\n * {@inheritDoc}\r\n */\r\n public async get(key: CacheKey) {\r\n key = this.parseKey(key);\r\n\r\n this.log(\"fetching\", key);\r\n\r\n const value = await this.client?.get(key);\r\n\r\n if (!value) {\r\n this.log(\"notFound\", key);\r\n // Emit miss event\r\n await this.emit(\"miss\", { key });\r\n return null;\r\n }\r\n\r\n this.log(\"fetched\", key);\r\n\r\n // Parse and return the value directly (Redis handles expiration natively)\r\n const parsedValue = JSON.parse(value);\r\n\r\n // Apply cloning for immutability protection\r\n if (parsedValue === null || parsedValue === undefined) {\r\n // Emit hit event\r\n await this.emit(\"hit\", { key, value: parsedValue });\r\n return parsedValue;\r\n }\r\n\r\n const type = typeof parsedValue;\r\n if (type === \"string\" || type === \"number\" || type === \"boolean\") {\r\n // Emit hit event\r\n await this.emit(\"hit\", { key, value: parsedValue });\r\n return parsedValue;\r\n }\r\n\r\n try {\r\n const clonedValue = structuredClone(parsedValue);\r\n // Emit hit event\r\n await this.emit(\"hit\", { key, value: clonedValue });\r\n return clonedValue;\r\n } catch (error) {\r\n this.logError(`Failed to clone cached value for ${key}`, error);\r\n throw error;\r\n }\r\n }\r\n\r\n /**\r\n * {@inheritDoc}\r\n */\r\n public async remove(key: CacheKey) {\r\n key = this.parseKey(key);\r\n\r\n this.log(\"removing\", key);\r\n\r\n // Drop the SWR sidecar alongside the main key — keeps metadata from\r\n // surviving a `remove` and confusing a later `swr` read.\r\n await this.client?.del([key, this.swrMetaKey(key)]);\r\n\r\n this.log(\"removed\", key);\r\n\r\n await this.emit(\"removed\", { key });\r\n }\r\n\r\n /**\r\n * {@inheritDoc}\r\n */\r\n public async flush() {\r\n this.log(\"flushing\");\r\n\r\n if (this.options.globalPrefix) {\r\n await this.removeNamespace(\"\");\r\n } else {\r\n await this.client?.flushAll();\r\n }\r\n\r\n this.log(\"flushed\");\r\n\r\n // Emit flushed event\r\n await this.emit(\"flushed\");\r\n }\r\n\r\n /**\r\n * {@inheritDoc}\r\n */\r\n public async connect() {\r\n if (this.clientDriver) return;\r\n\r\n if (!isModuleExists) {\r\n throw new Error(REDIS_INSTALL_INSTRUCTIONS);\r\n }\r\n\r\n const options = this.options;\r\n\r\n if (options && !options.url && options.host) {\r\n const auth =\r\n options.password || options.username ? `${options.username}:${options.password}@` : \"\";\r\n\r\n if (!options.url) {\r\n const host = options.host || \"localhost\";\r\n const port = options.port || 6379;\r\n options.url = `redis://${auth}${host}:${port}`;\r\n }\r\n }\r\n\r\n const clientOptions = {\r\n ...options,\r\n ...(this.options.clientOptions || {}),\r\n };\r\n\r\n try {\r\n this.log(\"connecting\");\r\n const { createClient } = RedisClient;\r\n\r\n this.client = createClient(clientOptions);\r\n\r\n this.client.on(\"error\", (error: Error) => {\r\n if ((error as any).code === \"ECONNREFUSED\") {\r\n this.log(\"connectionFailed\", error);\r\n } else {\r\n this.log(\"error\", error.message);\r\n }\r\n });\r\n\r\n await this.client.connect();\r\n\r\n this.log(\"connected\");\r\n await this.emit(\"connected\");\r\n } catch (error) {\r\n console.log(\"Err\", error);\r\n\r\n // Boot-time cache connection failure is unrecoverable in practice —\r\n // `fatal` aligns Redis with the cascade drivers and herald connector\r\n // for clean \"page on fatal only\" alerting.\r\n log.fatal(\"cache\", \"redis\", error);\r\n await this.emit(\"error\", { error });\r\n }\r\n }\r\n\r\n /**\r\n * {@inheritDoc}\r\n *\r\n * Guards against disconnecting when the client was never created. The base\r\n * `client` getter falls back to `this` when no client is set, so we check\r\n * the backing `clientDriver` directly — using `this.client` for this guard\r\n * would always be truthy and crash with \"this.quit is not a function\".\r\n */\r\n public async disconnect() {\r\n if (!this.clientDriver) {\r\n return;\r\n }\r\n\r\n this.log(\"disconnecting\");\r\n\r\n await this.clientDriver.quit();\r\n\r\n this.log(\"disconnected\");\r\n await this.emit(\"disconnected\");\r\n }\r\n\r\n /**\r\n * Atomic increment using Redis native INCRBY command\r\n * {@inheritdoc}\r\n */\r\n public async increment(key: CacheKey, value: number = 1): Promise<number> {\r\n const parsedKey = this.parseKey(key);\r\n\r\n this.log(\"caching\", parsedKey);\r\n\r\n const result = await this.client?.incrBy(parsedKey, value);\r\n\r\n this.log(\"cached\", parsedKey);\r\n\r\n // Emit set event\r\n await this.emit(\"set\", { key: parsedKey, value: result, ttl: undefined });\r\n\r\n return result || 0;\r\n }\r\n\r\n /**\r\n * Atomic decrement using Redis native DECRBY command\r\n * {@inheritdoc}\r\n */\r\n public async decrement(key: CacheKey, value: number = 1): Promise<number> {\r\n const parsedKey = this.parseKey(key);\r\n\r\n this.log(\"caching\", parsedKey);\r\n\r\n const result = await this.client?.decrBy(parsedKey, value);\r\n\r\n this.log(\"cached\", parsedKey);\r\n\r\n // Emit set event\r\n await this.emit(\"set\", { key: parsedKey, value: result, ttl: undefined });\r\n\r\n return result || 0;\r\n }\r\n\r\n /**\r\n * Set if not exists (atomic operation)\r\n * Returns true if key was set, false if key already existed\r\n */\r\n public async setNX(key: CacheKey, value: any, ttl?: number): Promise<boolean> {\r\n const parsedKey = this.parseKey(key);\r\n\r\n this.log(\"caching\", parsedKey);\r\n\r\n if (ttl === undefined) {\r\n ttl = this.ttl;\r\n }\r\n\r\n let result: string | null;\r\n\r\n // Use Redis native SET with NX option\r\n if (ttl && ttl !== Infinity) {\r\n result = await this.client?.set(parsedKey, JSON.stringify(value), {\r\n NX: true,\r\n EX: ttl,\r\n });\r\n } else {\r\n result = await this.client?.set(parsedKey, JSON.stringify(value), {\r\n NX: true,\r\n });\r\n }\r\n\r\n const wasSet = result === \"OK\";\r\n\r\n if (wasSet) {\r\n this.log(\"cached\", parsedKey);\r\n // Emit set event\r\n await this.emit(\"set\", { key: parsedKey, value, ttl });\r\n } else {\r\n this.log(\"notFound\", parsedKey);\r\n }\r\n\r\n return wasSet;\r\n }\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"redis-cache-driver.mjs","names":[],"sources":["../../../../../../../cache/src/drivers/redis-cache-driver.ts"],"sourcesContent":["import { log } from \"@warlock.js/logger\";\r\nimport type { createClient } from \"redis\";\r\nimport type {\r\n CacheData,\r\n CacheDriver,\r\n CacheKey,\r\n CacheSetOptions,\r\n CacheSetResult,\r\n CacheTtl,\r\n RedisOptions,\r\n} from \"../types\";\r\nimport { CacheConfigurationError, CacheUnsupportedError } from \"../types\";\r\nimport { safeErrorInfo } from \"../utils\";\r\nimport { BaseCacheDriver } from \"./base-cache-driver\";\r\n\r\n// ============================================================\r\n// Lazy-loaded Redis SDK Types\r\n// ============================================================\r\n\r\n/**\r\n * Cached Redis module (loaded once, reused)\r\n */\r\nlet RedisClient: typeof import(\"redis\");\r\n\r\nlet isModuleExists: boolean | null = null;\r\n\r\n/**\r\n * Installation instructions for Redis package\r\n */\r\nconst REDIS_INSTALL_INSTRUCTIONS = `\r\nRedis cache driver requires the redis package.\r\nInstall it with:\r\n\r\n npm install redis\r\n\r\nOr with your preferred package manager:\r\n\r\n pnpm add redis\r\n yarn add redis\r\n`.trim();\r\n\r\n/**\r\n * Load Redis module\r\n */\r\nasync function loadRedis() {\r\n try {\r\n RedisClient = await import(\"redis\");\r\n isModuleExists = true;\r\n } catch {\r\n isModuleExists = false;\r\n }\r\n}\r\n\r\nloadRedis();\r\n\r\n// ============================================================\r\n// RedisCacheDriver Class\r\n// ============================================================\r\n\r\nexport class RedisCacheDriver\r\n extends BaseCacheDriver<ReturnType<typeof createClient>, RedisOptions>\r\n implements CacheDriver<ReturnType<typeof createClient>, RedisOptions>\r\n{\r\n /**\r\n * Cache driver name\r\n */\r\n public name = \"redis\";\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public setOptions(options: RedisOptions) {\r\n if (!options.url && !options.host) {\r\n throw new CacheConfigurationError(\r\n \"Redis driver requires either 'url' or 'host' option to be configured.\",\r\n );\r\n }\r\n\r\n return super.setOptions(options);\r\n }\r\n\r\n /**\r\n * {@inheritDoc}\r\n */\r\n public async removeNamespace(namespace: string) {\r\n namespace = this.parseKey(namespace);\r\n\r\n this.log(\"clearing\", namespace);\r\n\r\n // Escape Redis glob metacharacters so a namespace carrying `*`/`?`/`[`\r\n // cannot widen the match and delete keys outside its own prefix.\r\n const pattern = namespace.replace(/[\\\\*?[\\]]/g, \"\\\\$&\");\r\n\r\n // `SCAN` (cursor-based, non-blocking) instead of `KEYS` — `KEYS` is O(N)\r\n // and blocks the single-threaded Redis event loop for the full scan,\r\n // which can stall every other tenant/consumer on a large keyspace.\r\n const keys: string[] = [];\r\n\r\n if (this.client) {\r\n for await (const key of this.client.scanIterator({\r\n MATCH: `${pattern}*`,\r\n COUNT: 100,\r\n })) {\r\n keys.push(key as unknown as string);\r\n }\r\n }\r\n\r\n if (keys.length === 0) {\r\n this.log(\"notFound\", namespace);\r\n return;\r\n }\r\n\r\n await this.client?.del(keys);\r\n\r\n this.log(\"cleared\", namespace);\r\n\r\n return keys;\r\n }\r\n\r\n /**\r\n * {@inheritDoc}\r\n */\r\n public async set(\r\n key: CacheKey,\r\n value: any,\r\n ttlOrOptions?: CacheTtl | CacheSetOptions,\r\n ): Promise<any> {\r\n const parsedKey = this.parseKey(key);\r\n const { ttl, tags, onConflict, vector, staleAt } = this.resolveSetOptions(ttlOrOptions);\r\n\r\n if (vector) {\r\n throw new CacheUnsupportedError(\r\n \"'redis' driver does not yet support similarity retrieval. Phase 2 (RediSearch) is on the backlog — use a memory driver or the 'pg' driver (with pgvector) for now.\",\r\n );\r\n }\r\n\r\n this.log(\"caching\", parsedKey);\r\n\r\n const serialized = JSON.stringify(value);\r\n const hasExpiry = Boolean(ttl) && ttl !== Infinity;\r\n\r\n let reply: string | null | undefined;\r\n\r\n if (onConflict === \"create\") {\r\n const options: { NX: true; EX?: number } = { NX: true };\r\n if (hasExpiry) {\r\n options.EX = ttl as number;\r\n }\r\n reply = await this.client?.set(parsedKey, serialized, options);\r\n } else if (onConflict === \"update\") {\r\n const options: { XX: true; EX?: number } = { XX: true };\r\n if (hasExpiry) {\r\n options.EX = ttl as number;\r\n }\r\n reply = await this.client?.set(parsedKey, serialized, options);\r\n } else if (hasExpiry) {\r\n reply = await this.client?.set(parsedKey, serialized, { EX: ttl as number });\r\n } else {\r\n reply = await this.client?.set(parsedKey, serialized);\r\n }\r\n\r\n const wasSet = reply === \"OK\";\r\n\r\n if ((onConflict === \"create\" || onConflict === \"update\") && !wasSet) {\r\n const existing = onConflict === \"create\" ? ((await this.get(key)) as any) : null;\r\n return { wasSet: false, existing } satisfies CacheSetResult;\r\n }\r\n\r\n if (tags && tags.length > 0) {\r\n await this.applyTags(parsedKey, tags);\r\n }\r\n\r\n if (staleAt !== undefined) {\r\n // Sidecar key for SWR freshness — keeps the main value JSON\r\n // backwards-compatible with entries written before SWR landed.\r\n const sidecarOptions: { EX?: number } = {};\r\n\r\n if (hasExpiry) {\r\n sidecarOptions.EX = ttl as number;\r\n }\r\n\r\n await this.client?.set(this.swrMetaKey(parsedKey), String(staleAt), sidecarOptions);\r\n }\r\n\r\n this.log(\"cached\", parsedKey);\r\n\r\n await this.emit(\"set\", { key: parsedKey, value, ttl });\r\n\r\n if (onConflict === \"create\" || onConflict === \"update\") {\r\n return { wasSet: true, existing: null } satisfies CacheSetResult;\r\n }\r\n\r\n return value;\r\n }\r\n\r\n /**\r\n * Build the sidecar key Redis uses to track SWR freshness without\r\n * wrapping the main value JSON.\r\n */\r\n protected swrMetaKey(parsedKey: string): string {\r\n return `__swrmeta:${parsedKey}`;\r\n }\r\n\r\n /**\r\n * Read the raw {@link CacheData} wrapper, fetching the value and the\r\n * SWR sidecar in parallel. Returns `null` when the main key is missing\r\n * or expired (Redis handles expiry natively, so the absence of the\r\n * value alone tells us).\r\n */\r\n protected async getEntry(key: CacheKey): Promise<CacheData | null> {\r\n const parsedKey = this.parseKey(key);\r\n\r\n const [valueRaw, staleAtRaw] = await Promise.all([\r\n this.client?.get(parsedKey),\r\n this.client?.get(this.swrMetaKey(parsedKey)),\r\n ]);\r\n\r\n if (!valueRaw) {\r\n return null;\r\n }\r\n\r\n const data = JSON.parse(valueRaw);\r\n const staleAt = staleAtRaw ? Number(staleAtRaw) : undefined;\r\n\r\n return staleAt !== undefined ? { data, staleAt } : { data };\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n *\r\n * Redis tracks expiry natively (the payload carries no `expiresAt`), so read\r\n * the remaining lifetime with the `TTL` command. Redis returns `-2` for a\r\n * missing key and `-1` for a key with no expiry.\r\n */\r\n protected async getRemainingTtl(key: CacheKey): Promise<number | undefined> {\r\n const parsedKey = this.parseKey(key);\r\n const ttl = await this.client?.ttl(parsedKey);\r\n\r\n if (ttl === undefined || ttl === -2) {\r\n return undefined;\r\n }\r\n\r\n if (ttl === -1) {\r\n return Infinity;\r\n }\r\n\r\n return ttl;\r\n }\r\n\r\n /**\r\n * {@inheritDoc}\r\n */\r\n public async get(key: CacheKey) {\r\n key = this.parseKey(key);\r\n\r\n this.log(\"fetching\", key);\r\n\r\n const value = await this.client?.get(key);\r\n\r\n if (!value) {\r\n this.log(\"notFound\", key);\r\n // Emit miss event\r\n await this.emit(\"miss\", { key });\r\n return null;\r\n }\r\n\r\n this.log(\"fetched\", key);\r\n\r\n // Parse and return the value directly (Redis handles expiration natively)\r\n const parsedValue = JSON.parse(value);\r\n\r\n // Apply cloning for immutability protection\r\n if (parsedValue === null || parsedValue === undefined) {\r\n // Emit hit event\r\n await this.emit(\"hit\", { key, value: parsedValue });\r\n return parsedValue;\r\n }\r\n\r\n const type = typeof parsedValue;\r\n if (type === \"string\" || type === \"number\" || type === \"boolean\") {\r\n // Emit hit event\r\n await this.emit(\"hit\", { key, value: parsedValue });\r\n return parsedValue;\r\n }\r\n\r\n try {\r\n const clonedValue = structuredClone(parsedValue);\r\n // Emit hit event\r\n await this.emit(\"hit\", { key, value: clonedValue });\r\n return clonedValue;\r\n } catch (error) {\r\n this.logError(`Failed to clone cached value for ${key}`, error);\r\n throw error;\r\n }\r\n }\r\n\r\n /**\r\n * {@inheritDoc}\r\n */\r\n public async remove(key: CacheKey) {\r\n key = this.parseKey(key);\r\n\r\n this.log(\"removing\", key);\r\n\r\n // Drop the SWR sidecar alongside the main key — keeps metadata from\r\n // surviving a `remove` and confusing a later `swr` read.\r\n await this.client?.del([key, this.swrMetaKey(key)]);\r\n\r\n this.log(\"removed\", key);\r\n\r\n await this.emit(\"removed\", { key });\r\n }\r\n\r\n /**\r\n * {@inheritDoc}\r\n */\r\n public async flush() {\r\n this.log(\"flushing\");\r\n\r\n if (this.options.globalPrefix) {\r\n await this.removeNamespace(\"\");\r\n } else {\r\n await this.client?.flushAll();\r\n }\r\n\r\n this.log(\"flushed\");\r\n\r\n // Emit flushed event\r\n await this.emit(\"flushed\");\r\n }\r\n\r\n /**\r\n * {@inheritDoc}\r\n */\r\n public async connect() {\r\n if (this.clientDriver) return;\r\n\r\n if (!isModuleExists) {\r\n throw new Error(REDIS_INSTALL_INSTRUCTIONS);\r\n }\r\n\r\n const options = this.options;\r\n\r\n if (options && !options.url && options.host) {\r\n const auth =\r\n options.password || options.username ? `${options.username}:${options.password}@` : \"\";\r\n\r\n if (!options.url) {\r\n const host = options.host || \"localhost\";\r\n const port = options.port || 6379;\r\n options.url = `redis://${auth}${host}:${port}`;\r\n }\r\n }\r\n\r\n const clientOptions = {\r\n ...options,\r\n ...(this.options.clientOptions || {}),\r\n };\r\n\r\n try {\r\n this.log(\"connecting\");\r\n const { createClient } = RedisClient;\r\n\r\n this.client = createClient(clientOptions);\r\n\r\n this.client.on(\"error\", (error: Error) => {\r\n // Never pass the raw `error` object to the logger — connection\r\n // errors can carry the connection URL (with password) in the\r\n // message/cause. Only the redacted message survives past this point.\r\n const { message } = safeErrorInfo(error);\r\n\r\n if ((error as any).code === \"ECONNREFUSED\") {\r\n this.log(\"connectionFailed\", message);\r\n } else {\r\n this.log(\"error\", message);\r\n }\r\n });\r\n\r\n await this.client.connect();\r\n\r\n this.log(\"connected\");\r\n await this.emit(\"connected\");\r\n } catch (error) {\r\n // Boot-time cache connection failure is unrecoverable in practice —\r\n // `fatal` aligns Redis with the cascade drivers and herald connector\r\n // for clean \"page on fatal only\" alerting. Only the redacted\r\n // `{ message, code }` shape is logged; the raw error (which may carry\r\n // the connection URL/password) never reaches stdout or the logger.\r\n log.fatal(\"cache\", \"redis\", \"Failed to connect\", safeErrorInfo(error));\r\n await this.emit(\"error\", { error });\r\n }\r\n }\r\n\r\n /**\r\n * {@inheritDoc}\r\n *\r\n * Guards against disconnecting when the client was never created. The base\r\n * `client` getter falls back to `this` when no client is set, so we check\r\n * the backing `clientDriver` directly — using `this.client` for this guard\r\n * would always be truthy and crash with \"this.quit is not a function\".\r\n */\r\n public async disconnect() {\r\n if (!this.clientDriver) {\r\n return;\r\n }\r\n\r\n this.log(\"disconnecting\");\r\n\r\n await this.clientDriver.quit();\r\n\r\n this.log(\"disconnected\");\r\n await this.emit(\"disconnected\");\r\n }\r\n\r\n /**\r\n * Atomic increment using Redis native INCRBY command\r\n * {@inheritdoc}\r\n */\r\n public async increment(key: CacheKey, value: number = 1): Promise<number> {\r\n const parsedKey = this.parseKey(key);\r\n\r\n this.log(\"caching\", parsedKey);\r\n\r\n const result = await this.client?.incrBy(parsedKey, value);\r\n\r\n this.log(\"cached\", parsedKey);\r\n\r\n // Emit set event\r\n await this.emit(\"set\", { key: parsedKey, value: result, ttl: undefined });\r\n\r\n return result || 0;\r\n }\r\n\r\n /**\r\n * Atomic decrement using Redis native DECRBY command\r\n * {@inheritdoc}\r\n */\r\n public async decrement(key: CacheKey, value: number = 1): Promise<number> {\r\n const parsedKey = this.parseKey(key);\r\n\r\n this.log(\"caching\", parsedKey);\r\n\r\n const result = await this.client?.decrBy(parsedKey, value);\r\n\r\n this.log(\"cached\", parsedKey);\r\n\r\n // Emit set event\r\n await this.emit(\"set\", { key: parsedKey, value: result, ttl: undefined });\r\n\r\n return result || 0;\r\n }\r\n\r\n /**\r\n * Set if not exists (atomic operation)\r\n * Returns true if key was set, false if key already existed\r\n */\r\n public async setNX(key: CacheKey, value: any, ttl?: number): Promise<boolean> {\r\n const parsedKey = this.parseKey(key);\r\n\r\n this.log(\"caching\", parsedKey);\r\n\r\n if (ttl === undefined) {\r\n ttl = this.ttl;\r\n }\r\n\r\n let result: string | null;\r\n\r\n // Use Redis native SET with NX option\r\n if (ttl && ttl !== Infinity) {\r\n result = await this.client?.set(parsedKey, JSON.stringify(value), {\r\n NX: true,\r\n EX: ttl,\r\n });\r\n } else {\r\n result = await this.client?.set(parsedKey, JSON.stringify(value), {\r\n NX: true,\r\n });\r\n }\r\n\r\n const wasSet = result === \"OK\";\r\n\r\n if (wasSet) {\r\n this.log(\"cached\", parsedKey);\r\n // Emit set event\r\n await this.emit(\"set\", { key: parsedKey, value, ttl });\r\n } else {\r\n this.log(\"notFound\", parsedKey);\r\n }\r\n\r\n return wasSet;\r\n }\r\n}\r\n"],"mappings":"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"}
package/esm/index.d.mts CHANGED
@@ -17,5 +17,5 @@ import { MemoryCacheList } from "./list/memory-cache-list.mjs";
17
17
  import { ScopedCache } from "./scoped-cache.mjs";
18
18
  import { TaggedCache } from "./tagged-cache.mjs";
19
19
  import { TaggedScopedCache } from "./tagged-scoped-cache.mjs";
20
- import { CACHE_FOR, cosineSimilarity, expiresAtToTtl, injectTags, mergeTagSets, normalizeToOptions, normalizeToRememberOptions, parseCacheKey, parseTtl, resolveTtl } from "./utils.mjs";
21
- export { BaseCacheDriver, CACHE_FOR, CacheCall, CacheConcurrencyError, CacheConfigurationError, CacheConfigurations, CacheConflictPolicy, CacheConnectionError, CacheData, CacheDriver, CacheDriverNotInitializedError, CacheError, CacheEventData, CacheEventHandler, CacheEventType, CacheKey, CacheListAccessor, CacheManager, CacheMetricsCollector, CacheMetricsSnapshot, CacheNamespaceOptions, CacheOperationType, CacheSetOptions, CacheSetResult, CacheSimilarHit, CacheSimilarOptions, CacheSwrOptions, CacheTtl, CacheUnsupportedError, CachedFn, CachedOptions, DriverClass, FileCacheDriver, FileCacheOptions, LRUMemoryCacheDriver, LRUMemoryCacheOptions, LockOptions, LockOutcome, MemoryCacheDriver, MemoryCacheList, MemoryCacheOptions, MemoryExtendedCacheDriver, MemoryExtendedCacheOptions, MockCacheDriver, MockCacheOptions, NormalizedCachedConfig, NormalizedSetOptions, NullCacheDriver, NullCacheDriverOptions, PgCacheDriver, PgCacheOptions, PgClientLike, RedisCacheDriver, RedisOptions, RememberOptions, ScopedCache, ScopedCacheContract, TaggedCache, TaggedCacheDriver, TaggedScopedCache, TaggedScopedCacheContract, cache, cached, cosineSimilarity, deriveAutoKey, expiresAtToTtl, injectTags, mergeTagSets, normalizeCachedArgs, normalizeToOptions, normalizeToRememberOptions, parseCacheKey, parseTtl, resolveTtl };
20
+ import { CACHE_FOR, cosineSimilarity, expiresAtToTtl, injectTags, mergeTagSets, normalizeToOptions, normalizeToRememberOptions, parseCacheKey, parseTtl, redactCredentials, resolveTtl, safeErrorInfo } from "./utils.mjs";
21
+ export { BaseCacheDriver, CACHE_FOR, CacheCall, CacheConcurrencyError, CacheConfigurationError, CacheConfigurations, CacheConflictPolicy, CacheConnectionError, CacheData, CacheDriver, CacheDriverNotInitializedError, CacheError, CacheEventData, CacheEventHandler, CacheEventType, CacheKey, CacheListAccessor, CacheManager, CacheMetricsCollector, CacheMetricsSnapshot, CacheNamespaceOptions, CacheOperationType, CacheSetOptions, CacheSetResult, CacheSimilarHit, CacheSimilarOptions, CacheSwrOptions, CacheTtl, CacheUnsupportedError, CachedFn, CachedOptions, DriverClass, FileCacheDriver, FileCacheOptions, LRUMemoryCacheDriver, LRUMemoryCacheOptions, LockOptions, LockOutcome, MemoryCacheDriver, MemoryCacheList, MemoryCacheOptions, MemoryExtendedCacheDriver, MemoryExtendedCacheOptions, MockCacheDriver, MockCacheOptions, NormalizedCachedConfig, NormalizedSetOptions, NullCacheDriver, NullCacheDriverOptions, PgCacheDriver, PgCacheOptions, PgClientLike, RedisCacheDriver, RedisOptions, RememberOptions, ScopedCache, ScopedCacheContract, TaggedCache, TaggedCacheDriver, TaggedScopedCache, TaggedScopedCacheContract, cache, cached, cosineSimilarity, deriveAutoKey, expiresAtToTtl, injectTags, mergeTagSets, normalizeCachedArgs, normalizeToOptions, normalizeToRememberOptions, parseCacheKey, parseTtl, redactCredentials, resolveTtl, safeErrorInfo };
package/esm/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { CacheMetricsCollector } from "./metrics.mjs";
2
2
  import { CacheConcurrencyError, CacheConfigurationError, CacheConnectionError, CacheDriverNotInitializedError, CacheError, CacheUnsupportedError } from "./types.mjs";
3
- import { CACHE_FOR, cosineSimilarity, expiresAtToTtl, injectTags, mergeTagSets, normalizeToOptions, normalizeToRememberOptions, parseCacheKey, parseTtl, resolveTtl } from "./utils.mjs";
3
+ import { CACHE_FOR, cosineSimilarity, expiresAtToTtl, injectTags, mergeTagSets, normalizeToOptions, normalizeToRememberOptions, parseCacheKey, parseTtl, redactCredentials, resolveTtl, safeErrorInfo } from "./utils.mjs";
4
4
  import { TaggedScopedCache } from "./tagged-scoped-cache.mjs";
5
5
  import { ScopedCache } from "./scoped-cache.mjs";
6
6
  import { CacheManager, cache } from "./cache-manager.mjs";
@@ -21,4 +21,4 @@ import { PgCacheDriver } from "./drivers/pg-cache-driver.mjs";
21
21
  import { RedisCacheDriver } from "./drivers/redis-cache-driver.mjs";
22
22
  import "./drivers/index.mjs";
23
23
 
24
- export { BaseCacheDriver, CACHE_FOR, CacheConcurrencyError, CacheConfigurationError, CacheConnectionError, CacheDriverNotInitializedError, CacheError, CacheManager, CacheMetricsCollector, CacheUnsupportedError, FileCacheDriver, LRUMemoryCacheDriver, MemoryCacheDriver, MemoryCacheList, MemoryExtendedCacheDriver, MockCacheDriver, NullCacheDriver, PgCacheDriver, RedisCacheDriver, ScopedCache, TaggedCache, TaggedScopedCache, cache, cached, cosineSimilarity, deriveAutoKey, expiresAtToTtl, injectTags, mergeTagSets, normalizeCachedArgs, normalizeToOptions, normalizeToRememberOptions, parseCacheKey, parseTtl, resolveTtl };
24
+ export { BaseCacheDriver, CACHE_FOR, CacheConcurrencyError, CacheConfigurationError, CacheConnectionError, CacheDriverNotInitializedError, CacheError, CacheManager, CacheMetricsCollector, CacheUnsupportedError, FileCacheDriver, LRUMemoryCacheDriver, MemoryCacheDriver, MemoryCacheList, MemoryExtendedCacheDriver, MockCacheDriver, NullCacheDriver, PgCacheDriver, RedisCacheDriver, ScopedCache, TaggedCache, TaggedScopedCache, cache, cached, cosineSimilarity, deriveAutoKey, expiresAtToTtl, injectTags, mergeTagSets, normalizeCachedArgs, normalizeToOptions, normalizeToRememberOptions, parseCacheKey, parseTtl, redactCredentials, resolveTtl, safeErrorInfo };
package/esm/utils.d.mts CHANGED
@@ -114,6 +114,22 @@ declare function injectTags<T extends {
114
114
  * cosineSimilarity([1, 0, 0], [0, 1, 0]); // 0
115
115
  */
116
116
  declare function cosineSimilarity(a: number[], b: number[]): number;
117
+ /**
118
+ * Strip embedded `user:pass@` credentials from a string before it reaches
119
+ * any log sink.
120
+ */
121
+ declare function redactCredentials(message: string): string;
122
+ /**
123
+ * Reduce an unknown thrown value to a log-safe `{ message, code? }` shape.
124
+ * Callers must never log the raw error object itself — it may carry the
125
+ * connection URL (with password) in the top-level message, `cause`, or
126
+ * driver-specific fields, all of which `console.log`/`util.inspect` would
127
+ * still print in full.
128
+ */
129
+ declare function safeErrorInfo(error: unknown): {
130
+ message: string;
131
+ code?: string;
132
+ };
117
133
  declare enum CACHE_FOR {
118
134
  /**
119
135
  * Cache for 30 Minutes (in seconds)
@@ -157,5 +173,5 @@ declare enum CACHE_FOR {
157
173
  ONE_YEAR = 31536000
158
174
  }
159
175
  //#endregion
160
- export { CACHE_FOR, cosineSimilarity, expiresAtToTtl, injectTags, mergeTagSets, normalizeToOptions, normalizeToRememberOptions, parseCacheKey, parseTtl, resolveTtl };
176
+ export { CACHE_FOR, cosineSimilarity, expiresAtToTtl, injectTags, mergeTagSets, normalizeToOptions, normalizeToRememberOptions, parseCacheKey, parseTtl, redactCredentials, resolveTtl, safeErrorInfo };
161
177
  //# sourceMappingURL=utils.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.mts","names":[],"sources":["../../../../../../cache/src/utils.ts"],"mappings":";;;;;AAQA;iBAAgB,aAAA,CACd,GAAA,EAAK,QAAQ,EACb,OAAA;EAAW,YAAA;AAAA;;;;;;AAA6C;AA+B1D;;;;AAAwC;AAsCxC;;;;AAAuD;iBAtCvC,QAAA,CAAS,KAAe,EAAR,QAAQ;;;;;;;;;;;;;iBAsCxB,cAAA,CAAe,SAAwB,WAAJ,IAAI;AA+CvD;;;;;;;;;AAAA,iBAzBgB,kBAAA,CACd,KAAA,GAAQ,QAAA,GAAW,eAAA,GAClB,eAAA;;;;AAyBe;AAuBlB;;;;;;;iBAzBgB,0BAAA,CACd,KAAA,GAAQ,QAAA,GAAW,eAAA,GAClB,eAAA;;;AA0Be;AAiClB;;;;AACU;AA4BV;;;iBAjEgB,UAAA,CACd,GAAA,EAAK,QAAA,cACL,SAAA,WAAoB,IAAI,cACxB,QAAA;;;;;;;;AAiEE;AA0BJ;;;;AAAyD;AAgCzD;iBA1FgB,YAAA,IACX,KAAK;;;;;;;;;;iBA4BM,UAAA;EAAuB,IAAA;AAAA,GACrC,OAAA,EAAS,CAAA,EACT,SAAA,aACC,CAAC;;AAkGM;;;;;;;;;;;;;;iBAxEM,gBAAA,CAAiB,CAAA,YAAa,CAAW;AAAA,aAgC7C,SAAA;;;;EAIV,SAAA;;;;EAIA,QAAA;;;;EAIA,QAAA;;;;EAIA,OAAA;;;;EAIA,QAAA;;;;EAIA,UAAA;;;;EAIA,SAAA;;;;EAIA,UAAA;;;;EAIA,UAAA;;;;EAIA,QAAA;AAAA"}
1
+ {"version":3,"file":"utils.d.mts","names":[],"sources":["../../../../../../cache/src/utils.ts"],"mappings":";;;;;AAQA;iBAAgB,aAAA,CACd,GAAA,EAAK,QAAQ,EACb,OAAA;EAAW,YAAA;AAAA;;;;;;AAA6C;AA+B1D;;;;AAAwC;AAsCxC;;;;AAAuD;iBAtCvC,QAAA,CAAS,KAAe,EAAR,QAAQ;;;;;;;;;;;;;iBAsCxB,cAAA,CAAe,SAAwB,WAAJ,IAAI;AA+CvD;;;;;;;;;AAAA,iBAzBgB,kBAAA,CACd,KAAA,GAAQ,QAAA,GAAW,eAAA,GAClB,eAAA;;;;AAyBe;AAuBlB;;;;;;;iBAzBgB,0BAAA,CACd,KAAA,GAAQ,QAAA,GAAW,eAAA,GAClB,eAAA;;;AA0Be;AAiClB;;;;AACU;AA4BV;;;iBAjEgB,UAAA,CACd,GAAA,EAAK,QAAA,cACL,SAAA,WAAoB,IAAI,cACxB,QAAA;;;;;;;;AAiEE;AA0BJ;;;;AAAyD;AA4CzD;iBAtGgB,YAAA,IACX,KAAK;;;AAqGuC;AAWjD;;;;;;iBApFgB,UAAA;EAAuB,IAAA;AAAA,GACrC,OAAA,EAAS,CAAA,EACT,SAAA,aACC,CAAC;AAgGJ;;;;;;;;;;;;;;;AAAA,iBAtEgB,gBAAA,CAAiB,CAAA,YAAa,CAAW;;;;;iBA4CzC,iBAAA,CAAkB,OAAe;;;;;;;;iBAWjC,aAAA,CAAc,KAAA;EAAmB,OAAA;EAAiB,IAAA;AAAA;AAAA,aAetD,SAAA;;;;EAIV,SAAA;;;;EAIA,QAAA;;;;EAIA,QAAA;;;;EAIA,OAAA;;;;EAIA,QAAA;;;;EAIA,UAAA;;;;EAIA,SAAA;;;;EAIA,UAAA;;;;EAIA,UAAA;;;;EAIA,QAAA;AAAA"}
package/esm/utils.mjs CHANGED
@@ -173,6 +173,36 @@ function cosineSimilarity(a, b) {
173
173
  if (normA === 0 || normB === 0) return 0;
174
174
  return dot / (Math.sqrt(normA) * Math.sqrt(normB));
175
175
  }
176
+ /**
177
+ * Matches userinfo credentials embedded in a connection URL
178
+ * (e.g. `redis://user:pass@host`). Node/Redis/Postgres client errors
179
+ * frequently echo the target connection string — including the password —
180
+ * back in `error.message` on connection failure.
181
+ */
182
+ const CREDENTIALS_IN_URL = /(:\/\/)[^\s/@]+:[^\s/@]+@/g;
183
+ /**
184
+ * Strip embedded `user:pass@` credentials from a string before it reaches
185
+ * any log sink.
186
+ */
187
+ function redactCredentials(message) {
188
+ return message.replace(CREDENTIALS_IN_URL, "$1[REDACTED]@");
189
+ }
190
+ /**
191
+ * Reduce an unknown thrown value to a log-safe `{ message, code? }` shape.
192
+ * Callers must never log the raw error object itself — it may carry the
193
+ * connection URL (with password) in the top-level message, `cause`, or
194
+ * driver-specific fields, all of which `console.log`/`util.inspect` would
195
+ * still print in full.
196
+ */
197
+ function safeErrorInfo(error) {
198
+ if (error instanceof Error) {
199
+ const info = { message: redactCredentials(error.message) };
200
+ const code = error.code;
201
+ if (code) info.code = String(code);
202
+ return info;
203
+ }
204
+ return { message: redactCredentials(String(error)) };
205
+ }
176
206
  let CACHE_FOR = /* @__PURE__ */ function(CACHE_FOR) {
177
207
  /**
178
208
  * Cache for 30 Minutes (in seconds)
@@ -218,5 +248,5 @@ let CACHE_FOR = /* @__PURE__ */ function(CACHE_FOR) {
218
248
  }({});
219
249
 
220
250
  //#endregion
221
- export { CACHE_FOR, cosineSimilarity, expiresAtToTtl, injectTags, mergeTagSets, normalizeToOptions, normalizeToRememberOptions, parseCacheKey, parseTtl, resolveTtl };
251
+ export { CACHE_FOR, cosineSimilarity, expiresAtToTtl, injectTags, mergeTagSets, normalizeToOptions, normalizeToRememberOptions, parseCacheKey, parseTtl, redactCredentials, resolveTtl, safeErrorInfo };
222
252
  //# sourceMappingURL=utils.mjs.map
package/esm/utils.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"utils.mjs","names":[],"sources":["../../../../../../cache/src/utils.ts"],"sourcesContent":["import { rtrim } from \"@mongez/reinforcements\";\r\nimport ms, { StringValue } from \"ms\";\r\nimport type { CacheKey, CacheSetOptions, CacheTtl, RememberOptions } from \"./types\";\r\nimport { CacheConfigurationError } from \"./types\";\r\n\r\n/**\r\n * Make a proper key for the cache\r\n */\r\nexport function parseCacheKey(\r\n key: CacheKey,\r\n options: { globalPrefix?: string | (() => string) } = {},\r\n): string {\r\n if (typeof key === \"object\") {\r\n key = JSON.stringify(key);\r\n }\r\n\r\n // remove any curly braces and double quotes along with []\r\n key = key.replace(/[{}\"[\\]]/g, \"\").replaceAll(/[:,]/g, \".\");\r\n\r\n const cachePrefix =\r\n typeof options.globalPrefix === \"function\" ? options.globalPrefix() : options.globalPrefix;\r\n\r\n return rtrim(String(cachePrefix ? rtrim(cachePrefix, \".\") + \".\" + key : key), \".\");\r\n}\r\n\r\n/**\r\n * Parse a TTL value into seconds.\r\n *\r\n * Accepts:\r\n * - a number (already in seconds) — returned unchanged\r\n * - `Infinity` — no expiration, returned unchanged\r\n * - a human-readable duration string (e.g. `\"1h\"`, `\"30m\"`, `\"7d\"`) — parsed via `ms` then converted to seconds\r\n *\r\n * Throws `CacheConfigurationError` on unparseable strings or negative numbers.\r\n *\r\n * @example\r\n * parseTtl(3600); // 3600\r\n * parseTtl(\"1h\"); // 3600\r\n * parseTtl(\"7d\"); // 604800\r\n * parseTtl(Infinity); // Infinity\r\n */\r\nexport function parseTtl(input: CacheTtl): number {\r\n if (typeof input === \"number\") {\r\n if (input < 0) {\r\n throw new CacheConfigurationError(`Invalid TTL: negative number (${input}).`);\r\n }\r\n\r\n return input;\r\n }\r\n\r\n if (typeof input !== \"string\" || input.trim() === \"\") {\r\n throw new CacheConfigurationError(\r\n `Invalid TTL: expected number or duration string, got ${typeof input}.`,\r\n );\r\n }\r\n\r\n const milliseconds = ms(input as StringValue);\r\n\r\n if (milliseconds === undefined || Number.isNaN(milliseconds)) {\r\n throw new CacheConfigurationError(\r\n `Invalid TTL duration string: \"${input}\". Expected forms like \"1h\", \"30m\", \"7d\".`,\r\n );\r\n }\r\n\r\n return Math.floor(milliseconds / 1000);\r\n}\r\n\r\n/**\r\n * Convert an absolute `expiresAt` (Date or epoch milliseconds) into a\r\n * relative TTL in seconds.\r\n *\r\n * Throws {@link CacheConfigurationError} when the deadline is in the past —\r\n * the caller almost certainly has a bug (stale timestamp, wrong unit, etc.)\r\n * and silently storing an already-expired entry would hide it.\r\n *\r\n * @example\r\n * expiresAtToTtl(new Date(Date.now() + 60_000)); // ~60\r\n * expiresAtToTtl(Date.now() + 30 * 60 * 1000); // ~1800\r\n */\r\nexport function expiresAtToTtl(expiresAt: number | Date): number {\r\n const deadline = expiresAt instanceof Date ? expiresAt.getTime() : expiresAt;\r\n const relativeMs = deadline - Date.now();\r\n\r\n if (relativeMs <= 0) {\r\n throw new CacheConfigurationError(\r\n `\\`expiresAt\\` must be in the future; got ${new Date(deadline).toISOString()}.`,\r\n );\r\n }\r\n\r\n return Math.ceil(relativeMs / 1000);\r\n}\r\n\r\n/**\r\n * Coerce the polymorphic 3rd `set` argument into a uniform `CacheSetOptions`\r\n * shape. Lets callers (and `BaseCacheDriver.resolveSetOptions`) skip per-shape\r\n * branching.\r\n *\r\n * - `undefined` / `null` → `{}` (resolves to driver-level defaults later)\r\n * - `number` / `string` (positional TTL) → `{ ttl }`\r\n * - already an options object → returned as-is\r\n */\r\nexport function normalizeToOptions(\r\n input?: CacheTtl | CacheSetOptions,\r\n): CacheSetOptions {\r\n if (input === undefined || input === null) {\r\n return {};\r\n }\r\n\r\n if (typeof input === \"number\" || typeof input === \"string\") {\r\n return { ttl: input };\r\n }\r\n\r\n return input;\r\n}\r\n\r\n/**\r\n * Sibling of {@link normalizeToOptions} for the `remember()` call site, where\r\n * the polymorphic 2nd argument is `CacheTtl | RememberOptions` (no `expiresAt`,\r\n * no `onConflict`). Returns the same shape so callers can `{ ...opts, ... }`\r\n * without branching.\r\n *\r\n * @example\r\n * normalizeToRememberOptions(60); // { ttl: 60 }\r\n * normalizeToRememberOptions(\"1h\"); // { ttl: \"1h\" }\r\n * normalizeToRememberOptions({ ttl: \"1h\", tags: [\"x\"] }); // returned as-is\r\n */\r\nexport function normalizeToRememberOptions(\r\n input?: CacheTtl | RememberOptions,\r\n): RememberOptions {\r\n if (input === undefined || input === null) {\r\n return {};\r\n }\r\n\r\n if (typeof input === \"number\" || typeof input === \"string\") {\r\n return { ttl: input };\r\n }\r\n\r\n return input;\r\n}\r\n\r\n/**\r\n * Resolve the final TTL in seconds for a `set` call. Precedence:\r\n *\r\n * 1. Caller's `ttl` (number or duration string) wins.\r\n * 2. Otherwise, caller's `expiresAt` is converted to relative seconds.\r\n * 3. Otherwise, `fallback` is used (driver-level default — typically\r\n * `Infinity` when no default is configured, meaning \"never expires\").\r\n *\r\n * Throws {@link CacheConfigurationError} when `ttl` and `expiresAt` are\r\n * supplied together (mutually exclusive).\r\n */\r\nexport function resolveTtl(\r\n ttl: CacheTtl | undefined,\r\n expiresAt: number | Date | undefined,\r\n fallback: number,\r\n): number {\r\n if (ttl !== undefined && expiresAt !== undefined) {\r\n throw new CacheConfigurationError(\r\n \"Cache set options cannot specify both `ttl` and `expiresAt` — choose one.\",\r\n );\r\n }\r\n\r\n if (ttl !== undefined) {\r\n return parseTtl(ttl);\r\n }\r\n\r\n if (expiresAt !== undefined) {\r\n return expiresAtToTtl(expiresAt);\r\n }\r\n\r\n return fallback;\r\n}\r\n\r\n/**\r\n * Combine any number of tag lists into a single deduped array, dropping\r\n * `undefined`/empty entries. Returns `undefined` when no tags survive — lets\r\n * callers skip emitting empty `tags: []` into option payloads.\r\n *\r\n * Used by scoped-cache merging where scope tags + handle tags + per-call tags\r\n * must union additively without duplicates.\r\n *\r\n * @example\r\n * mergeTagSets([\"a\", \"b\"], [\"b\", \"c\"]); // [\"a\", \"b\", \"c\"]\r\n * mergeTagSets(undefined, [\"x\"]); // [\"x\"]\r\n * mergeTagSets(undefined, undefined); // undefined\r\n * mergeTagSets([], []); // undefined\r\n */\r\nexport function mergeTagSets(\r\n ...lists: (string[] | undefined)[]\r\n): string[] | undefined {\r\n const flat: string[] = [];\r\n\r\n for (const list of lists) {\r\n if (!list || list.length === 0) {\r\n continue;\r\n }\r\n\r\n flat.push(...list);\r\n }\r\n\r\n if (flat.length === 0) {\r\n return undefined;\r\n }\r\n\r\n return Array.from(new Set(flat));\r\n}\r\n\r\n/**\r\n * Add extra tags to any option-bag that already shapes `tags?: string[]`.\r\n * Pure — clones the input shape, never mutates. Tags are appended (caller\r\n * is responsible for de-duplication if needed; pair with {@link mergeTagSets}).\r\n *\r\n * @example\r\n * injectTags({ ttl: \"1h\" }, [\"unread\"]); // { ttl: \"1h\", tags: [\"unread\"] }\r\n * injectTags({ tags: [\"a\"] }, [\"b\"]); // { tags: [\"a\", \"b\"] }\r\n */\r\nexport function injectTags<T extends { tags?: string[] }>(\r\n options: T,\r\n extraTags: string[],\r\n): T {\r\n if (extraTags.length === 0) {\r\n return options;\r\n }\r\n\r\n return {\r\n ...options,\r\n tags: [...(options.tags ?? []), ...extraTags],\r\n };\r\n}\r\n\r\n/**\r\n * Cosine similarity between two equal-length numeric vectors.\r\n *\r\n * Returns a value in `[-1, 1]` where `1` means perfectly aligned, `0` means\r\n * orthogonal, and `-1` means opposing. For typical embedding spaces (where\r\n * vectors live in the positive cone) the practical range is `[0, 1]`.\r\n *\r\n * Throws {@link CacheConfigurationError} on dimension mismatch — fail loud at\r\n * the call site rather than silently returning a misleading score. A zero-norm\r\n * vector on either side returns `0` (no defined direction to compare).\r\n *\r\n * @example\r\n * cosineSimilarity([1, 0, 0], [1, 0, 0]); // 1\r\n * cosineSimilarity([1, 0, 0], [0, 1, 0]); // 0\r\n */\r\nexport function cosineSimilarity(a: number[], b: number[]): number {\r\n if (a.length !== b.length) {\r\n throw new CacheConfigurationError(\r\n `Vector dimension mismatch: got ${a.length} and ${b.length}.`,\r\n );\r\n }\r\n\r\n if (a.length === 0) {\r\n throw new CacheConfigurationError(\r\n \"Vector dimension mismatch: empty vector cannot be compared.\",\r\n );\r\n }\r\n\r\n let dot = 0;\r\n let normA = 0;\r\n let normB = 0;\r\n\r\n for (let i = 0; i < a.length; i++) {\r\n const x = a[i];\r\n const y = b[i];\r\n dot += x * y;\r\n normA += x * x;\r\n normB += y * y;\r\n }\r\n\r\n if (normA === 0 || normB === 0) {\r\n return 0;\r\n }\r\n\r\n return dot / (Math.sqrt(normA) * Math.sqrt(normB));\r\n}\r\n\r\nexport enum CACHE_FOR {\r\n /**\r\n * Cache for 30 Minutes (in seconds)\r\n */\r\n HALF_HOUR = 1800,\r\n /**\r\n * Cache for 1 Hour (in seconds)\r\n */\r\n ONE_HOUR = 3600,\r\n /**\r\n * Cache for 12 Hours (in seconds)\r\n */\r\n HALF_DAY = 43200,\r\n /**\r\n * Cache for 24 Hours (in seconds)\r\n */\r\n ONE_DAY = 86400,\r\n /**\r\n * Cache for 7 Days (in seconds)\r\n */\r\n ONE_WEEK = 604800,\r\n /**\r\n * Cache for 15 Days (in seconds)\r\n */\r\n HALF_MONTH = 1296000,\r\n /**\r\n * Cache for 30 Days (in seconds)\r\n */\r\n ONE_MONTH = 2592000,\r\n /**\r\n * Cache for 60 Days (in seconds)\r\n */\r\n TWO_MONTHS = 5184000,\r\n /**\r\n * Cache for 180 Days (in seconds)\r\n */\r\n SIX_MONTHS = 15768000,\r\n /**\r\n * Cache for 365 Days (in seconds)\r\n */\r\n ONE_YEAR = 31536000,\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"utils.mjs","names":[],"sources":["../../../../../../cache/src/utils.ts"],"sourcesContent":["import { rtrim } from \"@mongez/reinforcements\";\r\nimport ms, { StringValue } from \"ms\";\r\nimport type { CacheKey, CacheSetOptions, CacheTtl, RememberOptions } from \"./types\";\r\nimport { CacheConfigurationError } from \"./types\";\r\n\r\n/**\r\n * Make a proper key for the cache\r\n */\r\nexport function parseCacheKey(\r\n key: CacheKey,\r\n options: { globalPrefix?: string | (() => string) } = {},\r\n): string {\r\n if (typeof key === \"object\") {\r\n key = JSON.stringify(key);\r\n }\r\n\r\n // remove any curly braces and double quotes along with []\r\n key = key.replace(/[{}\"[\\]]/g, \"\").replaceAll(/[:,]/g, \".\");\r\n\r\n const cachePrefix =\r\n typeof options.globalPrefix === \"function\" ? options.globalPrefix() : options.globalPrefix;\r\n\r\n return rtrim(String(cachePrefix ? rtrim(cachePrefix, \".\") + \".\" + key : key), \".\");\r\n}\r\n\r\n/**\r\n * Parse a TTL value into seconds.\r\n *\r\n * Accepts:\r\n * - a number (already in seconds) — returned unchanged\r\n * - `Infinity` — no expiration, returned unchanged\r\n * - a human-readable duration string (e.g. `\"1h\"`, `\"30m\"`, `\"7d\"`) — parsed via `ms` then converted to seconds\r\n *\r\n * Throws `CacheConfigurationError` on unparseable strings or negative numbers.\r\n *\r\n * @example\r\n * parseTtl(3600); // 3600\r\n * parseTtl(\"1h\"); // 3600\r\n * parseTtl(\"7d\"); // 604800\r\n * parseTtl(Infinity); // Infinity\r\n */\r\nexport function parseTtl(input: CacheTtl): number {\r\n if (typeof input === \"number\") {\r\n if (input < 0) {\r\n throw new CacheConfigurationError(`Invalid TTL: negative number (${input}).`);\r\n }\r\n\r\n return input;\r\n }\r\n\r\n if (typeof input !== \"string\" || input.trim() === \"\") {\r\n throw new CacheConfigurationError(\r\n `Invalid TTL: expected number or duration string, got ${typeof input}.`,\r\n );\r\n }\r\n\r\n const milliseconds = ms(input as StringValue);\r\n\r\n if (milliseconds === undefined || Number.isNaN(milliseconds)) {\r\n throw new CacheConfigurationError(\r\n `Invalid TTL duration string: \"${input}\". Expected forms like \"1h\", \"30m\", \"7d\".`,\r\n );\r\n }\r\n\r\n return Math.floor(milliseconds / 1000);\r\n}\r\n\r\n/**\r\n * Convert an absolute `expiresAt` (Date or epoch milliseconds) into a\r\n * relative TTL in seconds.\r\n *\r\n * Throws {@link CacheConfigurationError} when the deadline is in the past —\r\n * the caller almost certainly has a bug (stale timestamp, wrong unit, etc.)\r\n * and silently storing an already-expired entry would hide it.\r\n *\r\n * @example\r\n * expiresAtToTtl(new Date(Date.now() + 60_000)); // ~60\r\n * expiresAtToTtl(Date.now() + 30 * 60 * 1000); // ~1800\r\n */\r\nexport function expiresAtToTtl(expiresAt: number | Date): number {\r\n const deadline = expiresAt instanceof Date ? expiresAt.getTime() : expiresAt;\r\n const relativeMs = deadline - Date.now();\r\n\r\n if (relativeMs <= 0) {\r\n throw new CacheConfigurationError(\r\n `\\`expiresAt\\` must be in the future; got ${new Date(deadline).toISOString()}.`,\r\n );\r\n }\r\n\r\n return Math.ceil(relativeMs / 1000);\r\n}\r\n\r\n/**\r\n * Coerce the polymorphic 3rd `set` argument into a uniform `CacheSetOptions`\r\n * shape. Lets callers (and `BaseCacheDriver.resolveSetOptions`) skip per-shape\r\n * branching.\r\n *\r\n * - `undefined` / `null` → `{}` (resolves to driver-level defaults later)\r\n * - `number` / `string` (positional TTL) → `{ ttl }`\r\n * - already an options object → returned as-is\r\n */\r\nexport function normalizeToOptions(\r\n input?: CacheTtl | CacheSetOptions,\r\n): CacheSetOptions {\r\n if (input === undefined || input === null) {\r\n return {};\r\n }\r\n\r\n if (typeof input === \"number\" || typeof input === \"string\") {\r\n return { ttl: input };\r\n }\r\n\r\n return input;\r\n}\r\n\r\n/**\r\n * Sibling of {@link normalizeToOptions} for the `remember()` call site, where\r\n * the polymorphic 2nd argument is `CacheTtl | RememberOptions` (no `expiresAt`,\r\n * no `onConflict`). Returns the same shape so callers can `{ ...opts, ... }`\r\n * without branching.\r\n *\r\n * @example\r\n * normalizeToRememberOptions(60); // { ttl: 60 }\r\n * normalizeToRememberOptions(\"1h\"); // { ttl: \"1h\" }\r\n * normalizeToRememberOptions({ ttl: \"1h\", tags: [\"x\"] }); // returned as-is\r\n */\r\nexport function normalizeToRememberOptions(\r\n input?: CacheTtl | RememberOptions,\r\n): RememberOptions {\r\n if (input === undefined || input === null) {\r\n return {};\r\n }\r\n\r\n if (typeof input === \"number\" || typeof input === \"string\") {\r\n return { ttl: input };\r\n }\r\n\r\n return input;\r\n}\r\n\r\n/**\r\n * Resolve the final TTL in seconds for a `set` call. Precedence:\r\n *\r\n * 1. Caller's `ttl` (number or duration string) wins.\r\n * 2. Otherwise, caller's `expiresAt` is converted to relative seconds.\r\n * 3. Otherwise, `fallback` is used (driver-level default — typically\r\n * `Infinity` when no default is configured, meaning \"never expires\").\r\n *\r\n * Throws {@link CacheConfigurationError} when `ttl` and `expiresAt` are\r\n * supplied together (mutually exclusive).\r\n */\r\nexport function resolveTtl(\r\n ttl: CacheTtl | undefined,\r\n expiresAt: number | Date | undefined,\r\n fallback: number,\r\n): number {\r\n if (ttl !== undefined && expiresAt !== undefined) {\r\n throw new CacheConfigurationError(\r\n \"Cache set options cannot specify both `ttl` and `expiresAt` — choose one.\",\r\n );\r\n }\r\n\r\n if (ttl !== undefined) {\r\n return parseTtl(ttl);\r\n }\r\n\r\n if (expiresAt !== undefined) {\r\n return expiresAtToTtl(expiresAt);\r\n }\r\n\r\n return fallback;\r\n}\r\n\r\n/**\r\n * Combine any number of tag lists into a single deduped array, dropping\r\n * `undefined`/empty entries. Returns `undefined` when no tags survive — lets\r\n * callers skip emitting empty `tags: []` into option payloads.\r\n *\r\n * Used by scoped-cache merging where scope tags + handle tags + per-call tags\r\n * must union additively without duplicates.\r\n *\r\n * @example\r\n * mergeTagSets([\"a\", \"b\"], [\"b\", \"c\"]); // [\"a\", \"b\", \"c\"]\r\n * mergeTagSets(undefined, [\"x\"]); // [\"x\"]\r\n * mergeTagSets(undefined, undefined); // undefined\r\n * mergeTagSets([], []); // undefined\r\n */\r\nexport function mergeTagSets(\r\n ...lists: (string[] | undefined)[]\r\n): string[] | undefined {\r\n const flat: string[] = [];\r\n\r\n for (const list of lists) {\r\n if (!list || list.length === 0) {\r\n continue;\r\n }\r\n\r\n flat.push(...list);\r\n }\r\n\r\n if (flat.length === 0) {\r\n return undefined;\r\n }\r\n\r\n return Array.from(new Set(flat));\r\n}\r\n\r\n/**\r\n * Add extra tags to any option-bag that already shapes `tags?: string[]`.\r\n * Pure — clones the input shape, never mutates. Tags are appended (caller\r\n * is responsible for de-duplication if needed; pair with {@link mergeTagSets}).\r\n *\r\n * @example\r\n * injectTags({ ttl: \"1h\" }, [\"unread\"]); // { ttl: \"1h\", tags: [\"unread\"] }\r\n * injectTags({ tags: [\"a\"] }, [\"b\"]); // { tags: [\"a\", \"b\"] }\r\n */\r\nexport function injectTags<T extends { tags?: string[] }>(\r\n options: T,\r\n extraTags: string[],\r\n): T {\r\n if (extraTags.length === 0) {\r\n return options;\r\n }\r\n\r\n return {\r\n ...options,\r\n tags: [...(options.tags ?? []), ...extraTags],\r\n };\r\n}\r\n\r\n/**\r\n * Cosine similarity between two equal-length numeric vectors.\r\n *\r\n * Returns a value in `[-1, 1]` where `1` means perfectly aligned, `0` means\r\n * orthogonal, and `-1` means opposing. For typical embedding spaces (where\r\n * vectors live in the positive cone) the practical range is `[0, 1]`.\r\n *\r\n * Throws {@link CacheConfigurationError} on dimension mismatch — fail loud at\r\n * the call site rather than silently returning a misleading score. A zero-norm\r\n * vector on either side returns `0` (no defined direction to compare).\r\n *\r\n * @example\r\n * cosineSimilarity([1, 0, 0], [1, 0, 0]); // 1\r\n * cosineSimilarity([1, 0, 0], [0, 1, 0]); // 0\r\n */\r\nexport function cosineSimilarity(a: number[], b: number[]): number {\r\n if (a.length !== b.length) {\r\n throw new CacheConfigurationError(\r\n `Vector dimension mismatch: got ${a.length} and ${b.length}.`,\r\n );\r\n }\r\n\r\n if (a.length === 0) {\r\n throw new CacheConfigurationError(\r\n \"Vector dimension mismatch: empty vector cannot be compared.\",\r\n );\r\n }\r\n\r\n let dot = 0;\r\n let normA = 0;\r\n let normB = 0;\r\n\r\n for (let i = 0; i < a.length; i++) {\r\n const x = a[i];\r\n const y = b[i];\r\n dot += x * y;\r\n normA += x * x;\r\n normB += y * y;\r\n }\r\n\r\n if (normA === 0 || normB === 0) {\r\n return 0;\r\n }\r\n\r\n return dot / (Math.sqrt(normA) * Math.sqrt(normB));\r\n}\r\n\r\n/**\r\n * Matches userinfo credentials embedded in a connection URL\r\n * (e.g. `redis://user:pass@host`). Node/Redis/Postgres client errors\r\n * frequently echo the target connection string — including the password —\r\n * back in `error.message` on connection failure.\r\n */\r\nconst CREDENTIALS_IN_URL = /(:\\/\\/)[^\\s/@]+:[^\\s/@]+@/g;\r\n\r\n/**\r\n * Strip embedded `user:pass@` credentials from a string before it reaches\r\n * any log sink.\r\n */\r\nexport function redactCredentials(message: string): string {\r\n return message.replace(CREDENTIALS_IN_URL, \"$1[REDACTED]@\");\r\n}\r\n\r\n/**\r\n * Reduce an unknown thrown value to a log-safe `{ message, code? }` shape.\r\n * Callers must never log the raw error object itself — it may carry the\r\n * connection URL (with password) in the top-level message, `cause`, or\r\n * driver-specific fields, all of which `console.log`/`util.inspect` would\r\n * still print in full.\r\n */\r\nexport function safeErrorInfo(error: unknown): { message: string; code?: string } {\r\n if (error instanceof Error) {\r\n const info: { message: string; code?: string } = {\r\n message: redactCredentials(error.message),\r\n };\r\n\r\n const code = (error as any).code;\r\n if (code) info.code = String(code);\r\n\r\n return info;\r\n }\r\n\r\n return { message: redactCredentials(String(error)) };\r\n}\r\n\r\nexport enum CACHE_FOR {\r\n /**\r\n * Cache for 30 Minutes (in seconds)\r\n */\r\n HALF_HOUR = 1800,\r\n /**\r\n * Cache for 1 Hour (in seconds)\r\n */\r\n ONE_HOUR = 3600,\r\n /**\r\n * Cache for 12 Hours (in seconds)\r\n */\r\n HALF_DAY = 43200,\r\n /**\r\n * Cache for 24 Hours (in seconds)\r\n */\r\n ONE_DAY = 86400,\r\n /**\r\n * Cache for 7 Days (in seconds)\r\n */\r\n ONE_WEEK = 604800,\r\n /**\r\n * Cache for 15 Days (in seconds)\r\n */\r\n HALF_MONTH = 1296000,\r\n /**\r\n * Cache for 30 Days (in seconds)\r\n */\r\n ONE_MONTH = 2592000,\r\n /**\r\n * Cache for 60 Days (in seconds)\r\n */\r\n TWO_MONTHS = 5184000,\r\n /**\r\n * Cache for 180 Days (in seconds)\r\n */\r\n SIX_MONTHS = 15768000,\r\n /**\r\n * Cache for 365 Days (in seconds)\r\n */\r\n ONE_YEAR = 31536000,\r\n}\r\n"],"mappings":"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"}
package/llms-full.txt CHANGED
@@ -479,6 +479,7 @@ import {
479
479
  | `CacheDriverNotInitializedError` | Any data op called before `cache.init()` / `cache.use()` | Call `cache.init()` at app startup. Tests often forget this — add a `beforeEach`. |
480
480
  | `CacheUnsupportedError` | Driver doesn't implement the requested op. Today: `update` / `merge` on the file driver; `set({ vector })` and `similar()` on file / redis / pg-without-`vector`-config. | Switch driver (memory family for dev similarity, `pg` with `vector` config for production), or queue the op. |
481
481
  | `CacheConcurrencyError` | Declared for future optimistic-concurrency exhaustion on Redis `update()` | Not thrown today. Reserved for the v2.1 `WATCH`/`MULTI` implementation. |
482
+ | `CacheError` (file driver, path containment) | A key/namespace, after percent-encoding, still resolves outside the cache root — `"Cache key resolves outside the cache directory: ..."`. Last line of defense against path traversal on top of the encoding step; should not happen in normal use. | Treat as a programmer/attacker-input error — don't retry; investigate the key source. See [`@warlock.js/cache/pick-cache-driver/SKILL.md`](@warlock.js/cache/pick-cache-driver/SKILL.md) for the encoding contract. |
482
483
 
483
484
  ## Special case — `setNX` unsupported
484
485
 
@@ -787,6 +788,14 @@ options: {
787
788
  }
788
789
  ```
789
790
 
791
+ ## Key & namespace safety (file / redis)
792
+
793
+ Cache keys can carry untrusted input (e.g. a `cached()` auto-key derived from a request param), so every built-in driver treats a key as data, never as a path or a query fragment:
794
+
795
+ - **`file`** — a key/namespace maps to exactly one on-disk directory component: `%`, `/`, and `\` are percent-encoded before the path is built, so `../../etc` becomes an inert directory name instead of a traversal. The resolved path is also asserted to stay inside the configured cache root (throws `CacheError` otherwise) — a second, encoding-independent check. `removeNamespace` clears both the namespace's own directory and every dotted child (`ns`, `ns.*`), honoring `globalPrefix`.
796
+ - **`redis`** — `removeNamespace` escapes glob metacharacters (`*`, `?`, `[`, `\`) in the namespace before building its match pattern, and walks matches with a `SCAN` cursor (`scanIterator`) instead of the blocking `KEYS` command, so clearing a namespace on a large keyspace doesn't stall the event loop for other tenants.
797
+ - **Errors that echo a connection string** (Redis `connect()` failures, any driver's failed op) are logged through a `safeErrorInfo()` helper that redacts `scheme://user:pass@` credentials to `scheme://[REDACTED]@` and never prints the raw `Error` object — see [`@warlock.js/cache/handle-cache-errors/SKILL.md`](@warlock.js/cache/handle-cache-errors/SKILL.md).
798
+
790
799
  ## Registering a custom driver
791
800
 
792
801
  ```ts
@@ -1450,7 +1459,7 @@ await cache.set("user:2:profile", otherProfile);
1450
1459
  await cache.removeNamespace("user.1"); // drops both user:1 entries, keeps user:2
1451
1460
  ```
1452
1461
 
1453
- Cheaper than tags (no reverse index to maintain). Every real driver supports it — memory family and `lru` by prefix-scan, `file` by directory, `redis`/`pg` by key/`LIKE` prefix; `null` no-ops. See [`@warlock.js/cache/pick-cache-driver/SKILL.md`](@warlock.js/cache/pick-cache-driver/SKILL.md).
1462
+ Cheaper than tags (no reverse index to maintain). Every real driver supports it — memory family and `lru` by prefix-scan, `file` by directory (including dotted sibling keys like `ns.a`), `redis` by non-blocking `SCAN` (not `KEYS`), `pg` by key/`LIKE` prefix; `null` no-ops. Namespace strings are treated as data, not glob/path input — see [`@warlock.js/cache/pick-cache-driver/SKILL.md`](@warlock.js/cache/pick-cache-driver/SKILL.md) for the file-path-containment and redis-glob-escaping details.
1454
1463
 
1455
1464
  ## Multi-tenant scoping at the driver level
1456
1465
 
package/package.json CHANGED
@@ -2,9 +2,9 @@
2
2
  "name": "@warlock.js/cache",
3
3
  "description": "A Robust Cache Manager for Nodejs",
4
4
  "dependencies": {
5
- "@warlock.js/fs": "4.15.0",
6
- "@mongez/reinforcements": "^3.3.0",
7
- "@warlock.js/logger": "4.15.0",
5
+ "@warlock.js/fs": "4.16.0",
6
+ "@mongez/reinforcements": "^4.0.1",
7
+ "@warlock.js/logger": "4.16.0",
8
8
  "ms": "^2.1.3"
9
9
  },
10
10
  "peerDependencies": {
@@ -43,7 +43,7 @@
43
43
  ],
44
44
  "author": "hassanzohdy",
45
45
  "license": "MIT",
46
- "version": "4.15.0",
46
+ "version": "4.16.0",
47
47
  "main": "./cjs/index.cjs",
48
48
  "module": "./esm/index.mjs",
49
49
  "types": "./esm/index.d.mts",
@@ -26,6 +26,7 @@ import {
26
26
  | `CacheDriverNotInitializedError` | Any data op called before `cache.init()` / `cache.use()` | Call `cache.init()` at app startup. Tests often forget this — add a `beforeEach`. |
27
27
  | `CacheUnsupportedError` | Driver doesn't implement the requested op. Today: `update` / `merge` on the file driver; `set({ vector })` and `similar()` on file / redis / pg-without-`vector`-config. | Switch driver (memory family for dev similarity, `pg` with `vector` config for production), or queue the op. |
28
28
  | `CacheConcurrencyError` | Declared for future optimistic-concurrency exhaustion on Redis `update()` | Not thrown today. Reserved for the v2.1 `WATCH`/`MULTI` implementation. |
29
+ | `CacheError` (file driver, path containment) | A key/namespace, after percent-encoding, still resolves outside the cache root — `"Cache key resolves outside the cache directory: ..."`. Last line of defense against path traversal on top of the encoding step; should not happen in normal use. | Treat as a programmer/attacker-input error — don't retry; investigate the key source. See [`@warlock.js/cache/pick-cache-driver/SKILL.md`](@warlock.js/cache/pick-cache-driver/SKILL.md) for the encoding contract. |
29
30
 
30
31
  ## Special case — `setNX` unsupported
31
32
 
@@ -59,6 +59,14 @@ options: {
59
59
  }
60
60
  ```
61
61
 
62
+ ## Key & namespace safety (file / redis)
63
+
64
+ Cache keys can carry untrusted input (e.g. a `cached()` auto-key derived from a request param), so every built-in driver treats a key as data, never as a path or a query fragment:
65
+
66
+ - **`file`** — a key/namespace maps to exactly one on-disk directory component: `%`, `/`, and `\` are percent-encoded before the path is built, so `../../etc` becomes an inert directory name instead of a traversal. The resolved path is also asserted to stay inside the configured cache root (throws `CacheError` otherwise) — a second, encoding-independent check. `removeNamespace` clears both the namespace's own directory and every dotted child (`ns`, `ns.*`), honoring `globalPrefix`.
67
+ - **`redis`** — `removeNamespace` escapes glob metacharacters (`*`, `?`, `[`, `\`) in the namespace before building its match pattern, and walks matches with a `SCAN` cursor (`scanIterator`) instead of the blocking `KEYS` command, so clearing a namespace on a large keyspace doesn't stall the event loop for other tenants.
68
+ - **Errors that echo a connection string** (Redis `connect()` failures, any driver's failed op) are logged through a `safeErrorInfo()` helper that redacts `scheme://user:pass@` credentials to `scheme://[REDACTED]@` and never prints the raw `Error` object — see [`@warlock.js/cache/handle-cache-errors/SKILL.md`](@warlock.js/cache/handle-cache-errors/SKILL.md).
69
+
62
70
  ## Registering a custom driver
63
71
 
64
72
  ```ts
@@ -51,7 +51,7 @@ await cache.set("user:2:profile", otherProfile);
51
51
  await cache.removeNamespace("user.1"); // drops both user:1 entries, keeps user:2
52
52
  ```
53
53
 
54
- Cheaper than tags (no reverse index to maintain). Every real driver supports it — memory family and `lru` by prefix-scan, `file` by directory, `redis`/`pg` by key/`LIKE` prefix; `null` no-ops. See [`@warlock.js/cache/pick-cache-driver/SKILL.md`](@warlock.js/cache/pick-cache-driver/SKILL.md).
54
+ Cheaper than tags (no reverse index to maintain). Every real driver supports it — memory family and `lru` by prefix-scan, `file` by directory (including dotted sibling keys like `ns.a`), `redis` by non-blocking `SCAN` (not `KEYS`), `pg` by key/`LIKE` prefix; `null` no-ops. Namespace strings are treated as data, not glob/path input — see [`@warlock.js/cache/pick-cache-driver/SKILL.md`](@warlock.js/cache/pick-cache-driver/SKILL.md) for the file-path-containment and redis-glob-escaping details.
55
55
 
56
56
  ## Multi-tenant scoping at the driver level
57
57