@frontmcp/utils 1.9.1 → 1.9.2

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.
@@ -7,6 +7,12 @@
7
7
  export declare function getEnv(_key: string): string | undefined;
8
8
  export declare function getEnv(_key: string, defaultValue: string): string;
9
9
  export declare function getCwd(): string;
10
+ /**
11
+ * The NODE_ENV the page runs with: a `process` shim's live value first, then the value a
12
+ * bundler inlined for `process.env.NODE_ENV`, else none. `isProduction()`, `isDevelopment()`
13
+ * and the runtime context's `env` all read it (#770). As on Node, with no NODE_ENV the
14
+ * runtime context reports `development` while `isDevelopment()` stays false.
15
+ */
10
16
  export declare function getNodeEnv(): string | undefined;
11
17
  export declare function isProduction(): boolean;
12
18
  export declare function isDevelopment(): boolean;
@@ -50,6 +50,14 @@ function isBrowserWorkerScope() {
50
50
  }
51
51
 
52
52
  // libs/utils/src/env/browser-env.ts
53
+ var NODE_ENV_KEY = "NODE_ENV";
54
+ function bundlerDefinedNodeEnv() {
55
+ try {
56
+ return process.env["NODE_ENV"] || void 0;
57
+ } catch {
58
+ return void 0;
59
+ }
60
+ }
53
61
  function getEnv(_key, defaultValue) {
54
62
  return defaultValue;
55
63
  }
@@ -57,13 +65,14 @@ function getCwd() {
57
65
  return "/";
58
66
  }
59
67
  function getNodeEnv() {
60
- return void 0;
68
+ const shim = globalThis.process;
69
+ return shim?.env?.[NODE_ENV_KEY] || bundlerDefinedNodeEnv();
61
70
  }
62
71
  function isProduction() {
63
- return false;
72
+ return getNodeEnv() === "production";
64
73
  }
65
74
  function isDevelopment() {
66
- return false;
75
+ return getNodeEnv() === "development";
67
76
  }
68
77
  function getEnvFlag(_key) {
69
78
  return false;
@@ -57,5 +57,9 @@ export declare function checkEntryAvailability(availability: EntryAvailability |
57
57
  missingAxes: (keyof EntryAvailability)[];
58
58
  };
59
59
  export declare function detectRuntimeContext(): RuntimeContext;
60
+ /**
61
+ * The browser runtime context. Its `env` is read live from the same NODE_ENV as
62
+ * `isProduction()`, defaulting to `development` as on Node (#770).
63
+ */
60
64
  export declare function getRuntimeContext(): RuntimeContext;
61
65
  export declare function resetRuntimeContext(): void;
@@ -29,6 +29,22 @@ __export(browser_runtime_context_exports, {
29
29
  });
30
30
  module.exports = __toCommonJS(browser_runtime_context_exports);
31
31
  var import_lazy_zod = require("@frontmcp/lazy-zod");
32
+
33
+ // libs/utils/src/env/browser-env.ts
34
+ var NODE_ENV_KEY = "NODE_ENV";
35
+ function bundlerDefinedNodeEnv() {
36
+ try {
37
+ return process.env["NODE_ENV"] || void 0;
38
+ } catch {
39
+ return void 0;
40
+ }
41
+ }
42
+ function getNodeEnv() {
43
+ const shim = globalThis.process;
44
+ return shim?.env?.[NODE_ENV_KEY] || bundlerDefinedNodeEnv();
45
+ }
46
+
47
+ // libs/utils/src/env/browser-runtime-context.ts
32
48
  var entryAvailabilitySchema = import_lazy_zod.z.object({
33
49
  os: import_lazy_zod.z.array(import_lazy_zod.z.string().min(1)).optional(),
34
50
  platform: import_lazy_zod.z.array(import_lazy_zod.z.string().min(1)).optional(),
@@ -84,14 +100,24 @@ function detectRuntimeContext() {
84
100
  deployment: "standalone",
85
101
  provider: "bare",
86
102
  target: "browser",
87
- env: "production"
103
+ env: getNodeEnv() || "development"
88
104
  };
89
105
  }
90
- var browserContext = detectRuntimeContext();
106
+ var cached;
91
107
  function getRuntimeContext() {
92
- return browserContext;
108
+ if (!cached) {
109
+ const detected = detectRuntimeContext();
110
+ Object.defineProperty(detected, "env", {
111
+ get: () => getNodeEnv() || "development",
112
+ enumerable: true,
113
+ configurable: true
114
+ });
115
+ cached = detected;
116
+ }
117
+ return cached;
93
118
  }
94
119
  function resetRuntimeContext() {
120
+ cached = void 0;
95
121
  }
96
122
  // Annotate the CommonJS export names for ESM import in node:
97
123
  0 && (module.exports = {
@@ -17,6 +17,14 @@ function isBrowserWorkerScope() {
17
17
  }
18
18
 
19
19
  // libs/utils/src/env/browser-env.ts
20
+ var NODE_ENV_KEY = "NODE_ENV";
21
+ function bundlerDefinedNodeEnv() {
22
+ try {
23
+ return process.env["NODE_ENV"] || void 0;
24
+ } catch {
25
+ return void 0;
26
+ }
27
+ }
20
28
  function getEnv(_key, defaultValue) {
21
29
  return defaultValue;
22
30
  }
@@ -24,13 +32,14 @@ function getCwd() {
24
32
  return "/";
25
33
  }
26
34
  function getNodeEnv() {
27
- return void 0;
35
+ const shim = globalThis.process;
36
+ return shim?.env?.[NODE_ENV_KEY] || bundlerDefinedNodeEnv();
28
37
  }
29
38
  function isProduction() {
30
- return false;
39
+ return getNodeEnv() === "production";
31
40
  }
32
41
  function isDevelopment() {
33
- return false;
42
+ return getNodeEnv() === "development";
34
43
  }
35
44
  function getEnvFlag(_key) {
36
45
  return false;
@@ -3,6 +3,22 @@ const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.ur
3
3
  // __frontmcpCreateRequire
4
4
  // libs/utils/src/env/browser-runtime-context.ts
5
5
  import { z } from "@frontmcp/lazy-zod";
6
+
7
+ // libs/utils/src/env/browser-env.ts
8
+ var NODE_ENV_KEY = "NODE_ENV";
9
+ function bundlerDefinedNodeEnv() {
10
+ try {
11
+ return process.env["NODE_ENV"] || void 0;
12
+ } catch {
13
+ return void 0;
14
+ }
15
+ }
16
+ function getNodeEnv() {
17
+ const shim = globalThis.process;
18
+ return shim?.env?.[NODE_ENV_KEY] || bundlerDefinedNodeEnv();
19
+ }
20
+
21
+ // libs/utils/src/env/browser-runtime-context.ts
6
22
  var entryAvailabilitySchema = z.object({
7
23
  os: z.array(z.string().min(1)).optional(),
8
24
  platform: z.array(z.string().min(1)).optional(),
@@ -58,14 +74,24 @@ function detectRuntimeContext() {
58
74
  deployment: "standalone",
59
75
  provider: "bare",
60
76
  target: "browser",
61
- env: "production"
77
+ env: getNodeEnv() || "development"
62
78
  };
63
79
  }
64
- var browserContext = detectRuntimeContext();
80
+ var cached;
65
81
  function getRuntimeContext() {
66
- return browserContext;
82
+ if (!cached) {
83
+ const detected = detectRuntimeContext();
84
+ Object.defineProperty(detected, "env", {
85
+ get: () => getNodeEnv() || "development",
86
+ enumerable: true,
87
+ configurable: true
88
+ });
89
+ cached = detected;
90
+ }
91
+ return cached;
67
92
  }
68
93
  function resetRuntimeContext() {
94
+ cached = void 0;
69
95
  }
70
96
  export {
71
97
  checkEntryAvailability,
package/esm/index.mjs CHANGED
@@ -3015,6 +3015,55 @@ var init_redis_error_listener = __esm({
3015
3015
  }
3016
3016
  });
3017
3017
 
3018
+ // libs/utils/src/storage/redis-url.ts
3019
+ function decodeOrRaw(encoded) {
3020
+ try {
3021
+ return decodeURIComponent(encoded);
3022
+ } catch {
3023
+ return encoded;
3024
+ }
3025
+ }
3026
+ function mergeRedisUrlFields(rawUrl, fields) {
3027
+ let url;
3028
+ try {
3029
+ url = new URL(rawUrl);
3030
+ } catch {
3031
+ return void 0;
3032
+ }
3033
+ const urlHost = url.hostname.replace(/^\[(.*)\]$/, "$1");
3034
+ const urlPort = url.port ? Number(url.port) : void 0;
3035
+ const urlPassword = url.password ? decodeOrRaw(url.password) : void 0;
3036
+ const urlDbText = url.pathname.replace(/^\/+/, "") || url.searchParams.get("db") || void 0;
3037
+ const urlDb = urlDbText === void 0 ? void 0 : Number(urlDbText);
3038
+ const urlUsesTls = url.protocol === "rediss:";
3039
+ const fillIns = {};
3040
+ const conflicts = [];
3041
+ if (fields.host !== void 0 && fields.host !== urlHost) conflicts.push("host");
3042
+ if (fields.port !== void 0) {
3043
+ if (urlPort === void 0) fillIns.port = fields.port;
3044
+ else if (fields.port !== urlPort) conflicts.push("port");
3045
+ }
3046
+ if (fields.password !== void 0) {
3047
+ if (urlPassword === void 0) fillIns.password = fields.password;
3048
+ else if (fields.password !== urlPassword) conflicts.push("password");
3049
+ }
3050
+ if (fields.db !== void 0) {
3051
+ if (urlDb === void 0) fillIns.db = fields.db;
3052
+ else if (fields.db !== urlDb) conflicts.push("db");
3053
+ }
3054
+ if (fields.tls === true && !urlUsesTls) fillIns.tls = true;
3055
+ if (fields.tls === false && urlUsesTls) conflicts.push("tls");
3056
+ return { fillIns, conflicts };
3057
+ }
3058
+ function describeRedisUrlConflicts(conflicts) {
3059
+ return `redis ${conflicts.join(", ")} contradict${conflicts.length === 1 ? "s" : ""} redis.url. Fields beside a url only fill in what the URL leaves out (port, password, db, tls); put the value in the URL or drop the field.`;
3060
+ }
3061
+ var init_redis_url = __esm({
3062
+ "libs/utils/src/storage/redis-url.ts"() {
3063
+ "use strict";
3064
+ }
3065
+ });
3066
+
3018
3067
  // libs/utils/src/storage/utils/index.ts
3019
3068
  var init_utils = __esm({
3020
3069
  "libs/utils/src/storage/utils/index.ts"() {
@@ -3054,6 +3103,17 @@ function getRedisClass() {
3054
3103
  });
3055
3104
  }
3056
3105
  }
3106
+ function resolveUrlFillIns(connectionUrl, options) {
3107
+ const { url: configUrl, ...siblingFields } = { url: void 0, ...options.config };
3108
+ if (options.url && configUrl && configUrl !== options.url) {
3109
+ throw new StorageConfigError("redis", "redis.url and redis.config.url name different servers; set one of them.");
3110
+ }
3111
+ const merge = mergeRedisUrlFields(connectionUrl, siblingFields);
3112
+ if (merge && merge.conflicts.length > 0) {
3113
+ throw new StorageConfigError("redis", describeRedisUrlConflicts(merge.conflicts));
3114
+ }
3115
+ return merge?.fillIns ?? {};
3116
+ }
3057
3117
  function createRedisClient(options) {
3058
3118
  const RedisClass = getRedisClass();
3059
3119
  const baseOptions = { lazyConnect: false, maxRetriesPerRequest: 3 };
@@ -3074,6 +3134,7 @@ var init_redis = __esm({
3074
3134
  "use strict";
3075
3135
  init_errors();
3076
3136
  init_redis_error_listener();
3137
+ init_redis_url();
3077
3138
  init_utils();
3078
3139
  init_compare_and_delete();
3079
3140
  init_base();
@@ -3087,6 +3148,8 @@ var init_redis = __esm({
3087
3148
  connecting;
3088
3149
  keyPrefix;
3089
3150
  subscriptionHandlers = /* @__PURE__ */ new Map();
3151
+ connectionUrl;
3152
+ urlFillIns;
3090
3153
  constructor(options = {}) {
3091
3154
  super();
3092
3155
  const hasClient = options.client !== void 0;
@@ -3108,6 +3171,9 @@ var init_redis = __esm({
3108
3171
  this.options = options;
3109
3172
  this.ownsClient = !hasClient;
3110
3173
  this.keyPrefix = options.keyPrefix ?? "";
3174
+ const configUrl = options.config?.url;
3175
+ this.connectionUrl = options.url ?? (typeof configUrl === "string" ? configUrl : void 0);
3176
+ this.urlFillIns = this.connectionUrl ? resolveUrlFillIns(this.connectionUrl, options) : {};
3111
3177
  }
3112
3178
  // ============================================
3113
3179
  // Connection Lifecycle
@@ -3130,8 +3196,8 @@ var init_redis = __esm({
3130
3196
  client = this.options.client;
3131
3197
  } else {
3132
3198
  const RedisClass = getRedisClass();
3133
- if (this.options.url) {
3134
- client = new RedisClass(this.options.url, this.buildRedisOptions());
3199
+ if (this.connectionUrl) {
3200
+ client = new RedisClass(this.connectionUrl, this.buildRedisOptions());
3135
3201
  } else {
3136
3202
  client = new RedisClass(this.buildRedisOptions());
3137
3203
  }
@@ -3367,14 +3433,19 @@ var init_redis = __esm({
3367
3433
  * Build Redis options from config.
3368
3434
  */
3369
3435
  buildRedisOptions() {
3370
- if (this.options.url) {
3436
+ if (this.connectionUrl) {
3437
+ const { port, password, db, tls } = this.urlFillIns;
3371
3438
  return {
3439
+ ...port !== void 0 ? { port } : {},
3440
+ ...password !== void 0 ? { password } : {},
3441
+ ...db !== void 0 ? { db } : {},
3442
+ ...tls ? { tls: {} } : {},
3372
3443
  lazyConnect: false,
3373
3444
  maxRetriesPerRequest: 3
3374
3445
  };
3375
3446
  }
3376
3447
  const config = this.options.config;
3377
- if (!config) {
3448
+ if (!config || typeof config.url === "string" || config.host === void 0) {
3378
3449
  throw new StorageConfigError("redis", "Redis config is required when URL is not provided");
3379
3450
  }
3380
3451
  return {
@@ -3393,8 +3464,8 @@ var init_redis = __esm({
3393
3464
  async createSubscriber() {
3394
3465
  const RedisClass = getRedisClass();
3395
3466
  let subscriber;
3396
- if (this.options.url) {
3397
- subscriber = new RedisClass(this.options.url);
3467
+ if (this.connectionUrl) {
3468
+ subscriber = new RedisClass(this.connectionUrl, this.buildRedisOptions());
3398
3469
  } else if (this.options.config) {
3399
3470
  subscriber = new RedisClass(this.buildRedisOptions());
3400
3471
  } else if (this.options.client) {
@@ -3445,7 +3516,8 @@ var init_redis = __esm({
3445
3516
  // libs/utils/src/storage/adapters/vercel-kv.ts
3446
3517
  var vercel_kv_exports = {};
3447
3518
  __export(vercel_kv_exports, {
3448
- VercelKvStorageAdapter: () => VercelKvStorageAdapter
3519
+ VercelKvStorageAdapter: () => VercelKvStorageAdapter,
3520
+ createVercelKvClient: () => createVercelKvClient
3449
3521
  });
3450
3522
  async function loadVercelKv() {
3451
3523
  try {
@@ -3457,6 +3529,15 @@ async function loadVercelKv() {
3457
3529
  throw new Error("@vercel/kv is required for Vercel KV storage adapter. Install it with: npm install @vercel/kv");
3458
3530
  }
3459
3531
  }
3532
+ async function createVercelKvClient(connection = {}) {
3533
+ const url = connection.url ?? process.env["KV_REST_API_URL"];
3534
+ const token = connection.token ?? process.env["KV_REST_API_TOKEN"];
3535
+ if (!url || !token) {
3536
+ throw new StorageConfigError("vercel-kv", "Pass url and token, or set KV_REST_API_URL and KV_REST_API_TOKEN.");
3537
+ }
3538
+ const { createClient } = await loadVercelKv();
3539
+ return createClient({ url, token, cache: void 0, automaticDeserialization: false });
3540
+ }
3460
3541
  var VercelKvStorageAdapter;
3461
3542
  var init_vercel_kv = __esm({
3462
3543
  "libs/utils/src/storage/adapters/vercel-kv.ts"() {
@@ -3489,13 +3570,7 @@ var init_vercel_kv = __esm({
3489
3570
  async connect() {
3490
3571
  if (this.connected) return;
3491
3572
  try {
3492
- const { createClient } = await loadVercelKv();
3493
- const url = this.options.url;
3494
- const token = this.options.token;
3495
- if (!url || !token) {
3496
- throw new StorageConfigError("vercel-kv", "URL and token are required");
3497
- }
3498
- this.client = createClient({ url, token, cache: void 0, automaticDeserialization: false });
3573
+ this.client = await createVercelKvClient({ url: this.options.url, token: this.options.token });
3499
3574
  await this.client.exists("__healthcheck__");
3500
3575
  this.connected = true;
3501
3576
  } catch (e) {
@@ -3668,9 +3743,12 @@ var upstash_exports = {};
3668
3743
  __export(upstash_exports, {
3669
3744
  UpstashStorageAdapter: () => UpstashStorageAdapter
3670
3745
  });
3671
- function getUpstashRedis() {
3746
+ async function loadUpstashRedis() {
3672
3747
  try {
3673
- return __require("@upstash/redis");
3748
+ const mod = await import("@upstash/redis");
3749
+ const resolved = typeof mod.Redis === "function" ? mod : mod.default;
3750
+ if (!resolved || typeof resolved.Redis !== "function") throw new Error("Redis missing");
3751
+ return resolved;
3674
3752
  } catch {
3675
3753
  throw new Error(
3676
3754
  "@upstash/redis is required for Upstash storage adapter. Install it with: npm install @upstash/redis"
@@ -3715,7 +3793,7 @@ var init_upstash = __esm({
3715
3793
  async connect() {
3716
3794
  if (this.connected) return;
3717
3795
  try {
3718
- const { Redis } = getUpstashRedis();
3796
+ const { Redis } = await loadUpstashRedis();
3719
3797
  const url = this.options.url;
3720
3798
  const token = this.options.token;
3721
3799
  if (!url || !token) {
@@ -5243,6 +5321,7 @@ function getDetectedStorageType() {
5243
5321
 
5244
5322
  // libs/utils/src/storage/index.ts
5245
5323
  init_redis_error_listener();
5324
+ init_redis_url();
5246
5325
  init_errors();
5247
5326
 
5248
5327
  // libs/utils/src/storage/typed-storage.ts
@@ -5956,9 +6035,11 @@ export {
5956
6035
  createSafeRegExp,
5957
6036
  createSecretData,
5958
6037
  createStorage,
6038
+ createVercelKvClient,
5959
6039
  decryptAesGcm,
5960
6040
  decryptValue,
5961
6041
  deleteSecret,
6042
+ describeRedisUrlConflicts,
5962
6043
  deserializeAndDecrypt,
5963
6044
  deserializeBlob,
5964
6045
  detectRuntimeContext,
@@ -6042,6 +6123,7 @@ export {
6042
6123
  loadSecret,
6043
6124
  matchUriTemplate,
6044
6125
  matchesPattern,
6126
+ mergeRedisUrlFields,
6045
6127
  mkdir,
6046
6128
  mkdtemp,
6047
6129
  normalizeTTL,
package/index.d.ts CHANGED
@@ -26,8 +26,8 @@ export { EventEmitter } from './event-emitter';
26
26
  export { getEnv, getNodeEnv, getCwd, isProduction, isDevelopment, getEnvFlag, isDebug, setEnv, isEdgeRuntime, isServerless, supportsAnsi, getRuntimeContext, resetRuntimeContext, detectRuntimeContext, isEntryAvailable, checkEntryAvailability, entryAvailabilitySchema, } from './env';
27
27
  export type { RuntimeContext, EntryAvailability } from './env';
28
28
  export { analyzePattern, isPatternSafe, createSafeRegExp, safeTest, safeMatch, safeReplace, safeExec, isInputLengthSafe, DEFAULT_MAX_INPUT_LENGTH, REDOS_THRESHOLDS, trimLeading, trimTrailing, trimBoth, trimChars, extractBracedParams, expandTemplate, hasTemplatePlaceholders, collapseChar, collapseWhitespace, type SafeRegexOptions, type PatternAnalysisResult, } from './regex';
29
- export { createStorage, createMemoryStorage, getDetectedStorageType, NamespacedStorageImpl, createRootStorage, createNamespacedStorage, buildPrefix, NAMESPACE_SEPARATOR, StorageError, StorageConnectionError, StorageOperationError, StorageNotSupportedError, StorageConfigError, StorageTTLError, StoragePatternError, StorageNotConnectedError, EncryptedStorageError, BaseStorageAdapter, MemoryStorageAdapter, RedisStorageAdapter, createRedisClient, VercelKvStorageAdapter, UpstashStorageAdapter, FileSystemStorageAdapter, type FileSystemAdapterOptions, TypedStorage, type TypedStorageOptions, type TypedSetOptions, type TypedSetEntry, EncryptedTypedStorage, type EncryptedTypedStorageOptions, type EncryptedSetOptions, type EncryptedSetEntry, type EncryptionKey, type StoredEncryptedBlob, type ClientKeyBinding, globToRegex, matchesPattern, validatePattern, escapeGlob, MAX_TTL_SECONDS, validateTTL, validateOptionalTTL, ttlToExpiresAt, expiresAtToTTL, isExpired, normalizeTTL, attachRedisErrorListener, DEFAULT_REDIS_ERROR_LOG_INTERVAL_MS, } from './storage';
30
- export type { StorageAdapter, NamespacedStorage, RootStorage, SetOptions, SetEntry, MessageHandler, Unsubscribe, MemoryAdapterOptions, RedisAdapterOptions, VercelKvAdapterOptions, UpstashAdapterOptions, StorageType, StorageConfig, RedisErrorListenerOptions, CreateRedisClientOptions, ErrorEmitterClient, } from './storage';
29
+ export { createStorage, createMemoryStorage, getDetectedStorageType, NamespacedStorageImpl, createRootStorage, createNamespacedStorage, buildPrefix, NAMESPACE_SEPARATOR, StorageError, StorageConnectionError, StorageOperationError, StorageNotSupportedError, StorageConfigError, StorageTTLError, StoragePatternError, StorageNotConnectedError, EncryptedStorageError, BaseStorageAdapter, MemoryStorageAdapter, RedisStorageAdapter, createRedisClient, VercelKvStorageAdapter, createVercelKvClient, type VercelKvConnection, UpstashStorageAdapter, FileSystemStorageAdapter, type FileSystemAdapterOptions, TypedStorage, type TypedStorageOptions, type TypedSetOptions, type TypedSetEntry, EncryptedTypedStorage, type EncryptedTypedStorageOptions, type EncryptedSetOptions, type EncryptedSetEntry, type EncryptionKey, type StoredEncryptedBlob, type ClientKeyBinding, globToRegex, matchesPattern, validatePattern, escapeGlob, MAX_TTL_SECONDS, validateTTL, validateOptionalTTL, ttlToExpiresAt, expiresAtToTTL, isExpired, normalizeTTL, attachRedisErrorListener, DEFAULT_REDIS_ERROR_LOG_INTERVAL_MS, mergeRedisUrlFields, describeRedisUrlConflicts, } from './storage';
30
+ export type { StorageAdapter, NamespacedStorage, RootStorage, SetOptions, SetEntry, MessageHandler, Unsubscribe, MemoryAdapterOptions, RedisAdapterOptions, RedisHostConnectionConfig, RedisUrlConnectionConfig, RedisUrlFillInConfig, VercelKvAdapterOptions, UpstashAdapterOptions, StorageType, StorageConfig, RedisErrorListenerOptions, RedisUrlMerge, RedisUrlSiblingFields, CreateRedisClientOptions, ErrorEmitterClient, } from './storage';
31
31
  export { processPlatformToolCalls } from './llm';
32
32
  export type { CallToolFn, OpenAIToolCallItem, OpenAIToolResponse, ClaudeToolUseBlock, ClaudeToolResultBlock, VercelToolCallInfo, PlatformToolCallsInput, PlatformToolCallsOutput, SupportedPlatform, } from './llm';
33
33
  export { getMachineId, setMachineIdOverride } from './machine-id';
package/index.js CHANGED
@@ -3017,6 +3017,55 @@ var init_redis_error_listener = __esm({
3017
3017
  }
3018
3018
  });
3019
3019
 
3020
+ // libs/utils/src/storage/redis-url.ts
3021
+ function decodeOrRaw(encoded) {
3022
+ try {
3023
+ return decodeURIComponent(encoded);
3024
+ } catch {
3025
+ return encoded;
3026
+ }
3027
+ }
3028
+ function mergeRedisUrlFields(rawUrl, fields) {
3029
+ let url;
3030
+ try {
3031
+ url = new URL(rawUrl);
3032
+ } catch {
3033
+ return void 0;
3034
+ }
3035
+ const urlHost = url.hostname.replace(/^\[(.*)\]$/, "$1");
3036
+ const urlPort = url.port ? Number(url.port) : void 0;
3037
+ const urlPassword = url.password ? decodeOrRaw(url.password) : void 0;
3038
+ const urlDbText = url.pathname.replace(/^\/+/, "") || url.searchParams.get("db") || void 0;
3039
+ const urlDb = urlDbText === void 0 ? void 0 : Number(urlDbText);
3040
+ const urlUsesTls = url.protocol === "rediss:";
3041
+ const fillIns = {};
3042
+ const conflicts = [];
3043
+ if (fields.host !== void 0 && fields.host !== urlHost) conflicts.push("host");
3044
+ if (fields.port !== void 0) {
3045
+ if (urlPort === void 0) fillIns.port = fields.port;
3046
+ else if (fields.port !== urlPort) conflicts.push("port");
3047
+ }
3048
+ if (fields.password !== void 0) {
3049
+ if (urlPassword === void 0) fillIns.password = fields.password;
3050
+ else if (fields.password !== urlPassword) conflicts.push("password");
3051
+ }
3052
+ if (fields.db !== void 0) {
3053
+ if (urlDb === void 0) fillIns.db = fields.db;
3054
+ else if (fields.db !== urlDb) conflicts.push("db");
3055
+ }
3056
+ if (fields.tls === true && !urlUsesTls) fillIns.tls = true;
3057
+ if (fields.tls === false && urlUsesTls) conflicts.push("tls");
3058
+ return { fillIns, conflicts };
3059
+ }
3060
+ function describeRedisUrlConflicts(conflicts) {
3061
+ return `redis ${conflicts.join(", ")} contradict${conflicts.length === 1 ? "s" : ""} redis.url. Fields beside a url only fill in what the URL leaves out (port, password, db, tls); put the value in the URL or drop the field.`;
3062
+ }
3063
+ var init_redis_url = __esm({
3064
+ "libs/utils/src/storage/redis-url.ts"() {
3065
+ "use strict";
3066
+ }
3067
+ });
3068
+
3020
3069
  // libs/utils/src/storage/utils/index.ts
3021
3070
  var init_utils = __esm({
3022
3071
  "libs/utils/src/storage/utils/index.ts"() {
@@ -3056,6 +3105,17 @@ function getRedisClass() {
3056
3105
  });
3057
3106
  }
3058
3107
  }
3108
+ function resolveUrlFillIns(connectionUrl, options) {
3109
+ const { url: configUrl, ...siblingFields } = { url: void 0, ...options.config };
3110
+ if (options.url && configUrl && configUrl !== options.url) {
3111
+ throw new StorageConfigError("redis", "redis.url and redis.config.url name different servers; set one of them.");
3112
+ }
3113
+ const merge = mergeRedisUrlFields(connectionUrl, siblingFields);
3114
+ if (merge && merge.conflicts.length > 0) {
3115
+ throw new StorageConfigError("redis", describeRedisUrlConflicts(merge.conflicts));
3116
+ }
3117
+ return merge?.fillIns ?? {};
3118
+ }
3059
3119
  function createRedisClient(options) {
3060
3120
  const RedisClass = getRedisClass();
3061
3121
  const baseOptions = { lazyConnect: false, maxRetriesPerRequest: 3 };
@@ -3076,6 +3136,7 @@ var init_redis = __esm({
3076
3136
  "use strict";
3077
3137
  init_errors();
3078
3138
  init_redis_error_listener();
3139
+ init_redis_url();
3079
3140
  init_utils();
3080
3141
  init_compare_and_delete();
3081
3142
  init_base();
@@ -3089,6 +3150,8 @@ var init_redis = __esm({
3089
3150
  connecting;
3090
3151
  keyPrefix;
3091
3152
  subscriptionHandlers = /* @__PURE__ */ new Map();
3153
+ connectionUrl;
3154
+ urlFillIns;
3092
3155
  constructor(options = {}) {
3093
3156
  super();
3094
3157
  const hasClient = options.client !== void 0;
@@ -3110,6 +3173,9 @@ var init_redis = __esm({
3110
3173
  this.options = options;
3111
3174
  this.ownsClient = !hasClient;
3112
3175
  this.keyPrefix = options.keyPrefix ?? "";
3176
+ const configUrl = options.config?.url;
3177
+ this.connectionUrl = options.url ?? (typeof configUrl === "string" ? configUrl : void 0);
3178
+ this.urlFillIns = this.connectionUrl ? resolveUrlFillIns(this.connectionUrl, options) : {};
3113
3179
  }
3114
3180
  // ============================================
3115
3181
  // Connection Lifecycle
@@ -3132,8 +3198,8 @@ var init_redis = __esm({
3132
3198
  client = this.options.client;
3133
3199
  } else {
3134
3200
  const RedisClass = getRedisClass();
3135
- if (this.options.url) {
3136
- client = new RedisClass(this.options.url, this.buildRedisOptions());
3201
+ if (this.connectionUrl) {
3202
+ client = new RedisClass(this.connectionUrl, this.buildRedisOptions());
3137
3203
  } else {
3138
3204
  client = new RedisClass(this.buildRedisOptions());
3139
3205
  }
@@ -3369,14 +3435,19 @@ var init_redis = __esm({
3369
3435
  * Build Redis options from config.
3370
3436
  */
3371
3437
  buildRedisOptions() {
3372
- if (this.options.url) {
3438
+ if (this.connectionUrl) {
3439
+ const { port, password, db, tls } = this.urlFillIns;
3373
3440
  return {
3441
+ ...port !== void 0 ? { port } : {},
3442
+ ...password !== void 0 ? { password } : {},
3443
+ ...db !== void 0 ? { db } : {},
3444
+ ...tls ? { tls: {} } : {},
3374
3445
  lazyConnect: false,
3375
3446
  maxRetriesPerRequest: 3
3376
3447
  };
3377
3448
  }
3378
3449
  const config = this.options.config;
3379
- if (!config) {
3450
+ if (!config || typeof config.url === "string" || config.host === void 0) {
3380
3451
  throw new StorageConfigError("redis", "Redis config is required when URL is not provided");
3381
3452
  }
3382
3453
  return {
@@ -3395,8 +3466,8 @@ var init_redis = __esm({
3395
3466
  async createSubscriber() {
3396
3467
  const RedisClass = getRedisClass();
3397
3468
  let subscriber;
3398
- if (this.options.url) {
3399
- subscriber = new RedisClass(this.options.url);
3469
+ if (this.connectionUrl) {
3470
+ subscriber = new RedisClass(this.connectionUrl, this.buildRedisOptions());
3400
3471
  } else if (this.options.config) {
3401
3472
  subscriber = new RedisClass(this.buildRedisOptions());
3402
3473
  } else if (this.options.client) {
@@ -3447,7 +3518,8 @@ var init_redis = __esm({
3447
3518
  // libs/utils/src/storage/adapters/vercel-kv.ts
3448
3519
  var vercel_kv_exports = {};
3449
3520
  __export(vercel_kv_exports, {
3450
- VercelKvStorageAdapter: () => VercelKvStorageAdapter
3521
+ VercelKvStorageAdapter: () => VercelKvStorageAdapter,
3522
+ createVercelKvClient: () => createVercelKvClient
3451
3523
  });
3452
3524
  async function loadVercelKv() {
3453
3525
  try {
@@ -3459,6 +3531,15 @@ async function loadVercelKv() {
3459
3531
  throw new Error("@vercel/kv is required for Vercel KV storage adapter. Install it with: npm install @vercel/kv");
3460
3532
  }
3461
3533
  }
3534
+ async function createVercelKvClient(connection = {}) {
3535
+ const url = connection.url ?? process.env["KV_REST_API_URL"];
3536
+ const token = connection.token ?? process.env["KV_REST_API_TOKEN"];
3537
+ if (!url || !token) {
3538
+ throw new StorageConfigError("vercel-kv", "Pass url and token, or set KV_REST_API_URL and KV_REST_API_TOKEN.");
3539
+ }
3540
+ const { createClient } = await loadVercelKv();
3541
+ return createClient({ url, token, cache: void 0, automaticDeserialization: false });
3542
+ }
3462
3543
  var VercelKvStorageAdapter;
3463
3544
  var init_vercel_kv = __esm({
3464
3545
  "libs/utils/src/storage/adapters/vercel-kv.ts"() {
@@ -3491,13 +3572,7 @@ var init_vercel_kv = __esm({
3491
3572
  async connect() {
3492
3573
  if (this.connected) return;
3493
3574
  try {
3494
- const { createClient } = await loadVercelKv();
3495
- const url = this.options.url;
3496
- const token = this.options.token;
3497
- if (!url || !token) {
3498
- throw new StorageConfigError("vercel-kv", "URL and token are required");
3499
- }
3500
- this.client = createClient({ url, token, cache: void 0, automaticDeserialization: false });
3575
+ this.client = await createVercelKvClient({ url: this.options.url, token: this.options.token });
3501
3576
  await this.client.exists("__healthcheck__");
3502
3577
  this.connected = true;
3503
3578
  } catch (e) {
@@ -3670,9 +3745,12 @@ var upstash_exports = {};
3670
3745
  __export(upstash_exports, {
3671
3746
  UpstashStorageAdapter: () => UpstashStorageAdapter
3672
3747
  });
3673
- function getUpstashRedis() {
3748
+ async function loadUpstashRedis() {
3674
3749
  try {
3675
- return require("@upstash/redis");
3750
+ const mod = await import("@upstash/redis");
3751
+ const resolved = typeof mod.Redis === "function" ? mod : mod.default;
3752
+ if (!resolved || typeof resolved.Redis !== "function") throw new Error("Redis missing");
3753
+ return resolved;
3676
3754
  } catch {
3677
3755
  throw new Error(
3678
3756
  "@upstash/redis is required for Upstash storage adapter. Install it with: npm install @upstash/redis"
@@ -3717,7 +3795,7 @@ var init_upstash = __esm({
3717
3795
  async connect() {
3718
3796
  if (this.connected) return;
3719
3797
  try {
3720
- const { Redis } = getUpstashRedis();
3798
+ const { Redis } = await loadUpstashRedis();
3721
3799
  const url = this.options.url;
3722
3800
  const token = this.options.token;
3723
3801
  if (!url || !token) {
@@ -4184,9 +4262,11 @@ __export(index_exports, {
4184
4262
  createSafeRegExp: () => createSafeRegExp,
4185
4263
  createSecretData: () => createSecretData,
4186
4264
  createStorage: () => createStorage,
4265
+ createVercelKvClient: () => createVercelKvClient,
4187
4266
  decryptAesGcm: () => decryptAesGcm,
4188
4267
  decryptValue: () => decryptValue,
4189
4268
  deleteSecret: () => deleteSecret,
4269
+ describeRedisUrlConflicts: () => describeRedisUrlConflicts,
4190
4270
  deserializeAndDecrypt: () => deserializeAndDecrypt,
4191
4271
  deserializeBlob: () => deserializeBlob,
4192
4272
  detectRuntimeContext: () => import_runtime_context.detectRuntimeContext,
@@ -4270,6 +4350,7 @@ __export(index_exports, {
4270
4350
  loadSecret: () => loadSecret,
4271
4351
  matchUriTemplate: () => matchUriTemplate,
4272
4352
  matchesPattern: () => matchesPattern,
4353
+ mergeRedisUrlFields: () => mergeRedisUrlFields,
4273
4354
  mkdir: () => mkdir,
4274
4355
  mkdtemp: () => mkdtemp,
4275
4356
  normalizeTTL: () => normalizeTTL,
@@ -5446,6 +5527,7 @@ function getDetectedStorageType() {
5446
5527
 
5447
5528
  // libs/utils/src/storage/index.ts
5448
5529
  init_redis_error_listener();
5530
+ init_redis_url();
5449
5531
  init_errors();
5450
5532
 
5451
5533
  // libs/utils/src/storage/typed-storage.ts
@@ -6160,9 +6242,11 @@ function setMachineIdOverride(id) {
6160
6242
  createSafeRegExp,
6161
6243
  createSecretData,
6162
6244
  createStorage,
6245
+ createVercelKvClient,
6163
6246
  decryptAesGcm,
6164
6247
  decryptValue,
6165
6248
  deleteSecret,
6249
+ describeRedisUrlConflicts,
6166
6250
  deserializeAndDecrypt,
6167
6251
  deserializeBlob,
6168
6252
  detectRuntimeContext,
@@ -6246,6 +6330,7 @@ function setMachineIdOverride(id) {
6246
6330
  loadSecret,
6247
6331
  matchUriTemplate,
6248
6332
  matchesPattern,
6333
+ mergeRedisUrlFields,
6249
6334
  mkdir,
6250
6335
  mkdtemp,
6251
6336
  normalizeTTL,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/utils",
3
- "version": "1.9.1",
3
+ "version": "1.9.2",
4
4
  "description": "Shared utility functions for FrontMCP - string manipulation, URI handling, path utilities, and more",
5
5
  "author": "AgentFront <info@agentfront.dev>",
6
6
  "license": "Apache-2.0",
@@ -32,7 +32,7 @@
32
32
  "@noble/hashes": "^2.0.1"
33
33
  },
34
34
  "peerDependencies": {
35
- "@frontmcp/lazy-zod": "1.9.1",
35
+ "@frontmcp/lazy-zod": "1.9.2",
36
36
  "@vercel/kv": "^2.0.0 || ^3.0.0",
37
37
  "ioredis": "^5.0.0"
38
38
  },
@@ -7,7 +7,7 @@ export { BaseStorageAdapter } from './base';
7
7
  export { MemoryStorageAdapter } from './memory';
8
8
  export { RedisStorageAdapter, createRedisClient } from './redis';
9
9
  export type { CreateRedisClientOptions } from './redis';
10
- export { VercelKvStorageAdapter } from './vercel-kv';
10
+ export { VercelKvStorageAdapter, createVercelKvClient, type VercelKvConnection } from './vercel-kv';
11
11
  export { UpstashStorageAdapter } from './upstash';
12
12
  export { CloudflareKvStorageAdapter } from './cloudflare-kv';
13
13
  export { FileSystemStorageAdapter } from './filesystem';
@@ -39,6 +39,8 @@ export declare class RedisStorageAdapter extends BaseStorageAdapter {
39
39
  private connecting?;
40
40
  private readonly keyPrefix;
41
41
  private readonly subscriptionHandlers;
42
+ private readonly connectionUrl?;
43
+ private readonly urlFillIns;
42
44
  constructor(options?: RedisAdapterOptions);
43
45
  connect(): Promise<void>;
44
46
  private openConnection;
@@ -6,6 +6,46 @@
6
6
  */
7
7
  import type { SetOptions, VercelKvAdapterOptions } from '../types';
8
8
  import { BaseStorageAdapter } from './base';
9
+ type VercelKvClient = {
10
+ get: (key: string) => Promise<string | null>;
11
+ set: (key: string, value: string, options?: {
12
+ ex?: number;
13
+ nx?: boolean;
14
+ xx?: boolean;
15
+ }) => Promise<string | null>;
16
+ del: (...keys: string[]) => Promise<number>;
17
+ exists: (...keys: string[]) => Promise<number>;
18
+ mget: (...keys: string[]) => Promise<(string | null)[]>;
19
+ expire: (key: string, seconds: number) => Promise<number>;
20
+ ttl: (key: string) => Promise<number>;
21
+ incr: (key: string) => Promise<number>;
22
+ decr: (key: string) => Promise<number>;
23
+ incrby: (key: string, increment: number) => Promise<number>;
24
+ eval: (script: string, keys: string[], args: string[]) => Promise<unknown>;
25
+ scan: (cursor: number, options?: {
26
+ match?: string;
27
+ count?: number;
28
+ }) => Promise<[string, string[]]>;
29
+ keys: (pattern: string) => Promise<string[]>;
30
+ };
31
+ /** Where a Vercel KV client connects; both default to the `KV_REST_API_URL` / `KV_REST_API_TOKEN` variables. */
32
+ export interface VercelKvConnection {
33
+ url?: string;
34
+ token?: string;
35
+ }
36
+ /**
37
+ * Build a `@vercel/kv` REST client, the one loader every FrontMCP Vercel KV store shares (#711).
38
+ *
39
+ * `@vercel/kv` sets `cache: 'default'` on every request, which Cloudflare Workers reject ("The
40
+ * 'cache' field on 'RequestInitializerDict' is not implemented" / "Unsupported cache mode:
41
+ * default"), and the module's `kv` singleton cannot take another value. So each store gets a client
42
+ * of its own with the fetch cache mode unset (the value `@vercel/kv` itself calls equivalent), and
43
+ * values are read back exactly as stored instead of being JSON-parsed.
44
+ *
45
+ * @param connection - URL and token; each falls back to its environment variable
46
+ * @returns The client, typed as the subset of it the caller uses
47
+ */
48
+ export declare function createVercelKvClient<TClient = VercelKvClient>(connection?: VercelKvConnection): Promise<TClient>;
9
49
  /**
10
50
  * Vercel KV storage adapter.
11
51
  *
@@ -68,3 +108,4 @@ export declare class VercelKvStorageAdapter extends BaseStorageAdapter {
68
108
  */
69
109
  private unprefixKey;
70
110
  }
111
+ export {};
@@ -4,17 +4,19 @@
4
4
  * Unified storage abstraction with pluggable backends.
5
5
  * Supports Memory (dev), Redis (prod), Vercel KV (edge), and Upstash (edge + pub/sub).
6
6
  */
7
- export type { StorageAdapter, NamespacedStorage, RootStorage, SetOptions, SetEntry, MessageHandler, Unsubscribe, MemoryAdapterOptions, RedisAdapterOptions, VercelKvAdapterOptions, UpstashAdapterOptions, StorageType, StorageConfig, } from './types';
7
+ export type { StorageAdapter, NamespacedStorage, RootStorage, SetOptions, SetEntry, MessageHandler, Unsubscribe, MemoryAdapterOptions, RedisAdapterOptions, RedisHostConnectionConfig, RedisUrlConnectionConfig, RedisUrlFillInConfig, VercelKvAdapterOptions, UpstashAdapterOptions, StorageType, StorageConfig, } from './types';
8
8
  export { createStorage, createMemoryStorage, getDetectedStorageType } from './factory';
9
9
  export { attachRedisErrorListener, DEFAULT_REDIS_ERROR_LOG_INTERVAL_MS } from './redis-error-listener';
10
10
  export type { RedisErrorListenerOptions, ErrorEmitterClient } from './redis-error-listener';
11
+ export { mergeRedisUrlFields, describeRedisUrlConflicts } from './redis-url';
12
+ export type { RedisUrlMerge, RedisUrlSiblingFields } from './redis-url';
11
13
  export { NamespacedStorageImpl, createRootStorage, createNamespacedStorage, buildPrefix, NAMESPACE_SEPARATOR, } from './namespace';
12
14
  export { StorageError, StorageConnectionError, StorageOperationError, StorageNotSupportedError, StorageConfigError, StorageTTLError, StoragePatternError, StorageNotConnectedError, EncryptedStorageError, } from './errors';
13
15
  export { TypedStorage } from './typed-storage';
14
16
  export type { TypedStorageOptions, TypedSetOptions, TypedSetEntry } from './typed-storage.types';
15
17
  export { EncryptedTypedStorage } from './encrypted-typed-storage';
16
18
  export type { EncryptedTypedStorageOptions, EncryptedSetOptions, EncryptedSetEntry, EncryptionKey, StoredEncryptedBlob, ClientKeyBinding, } from './encrypted-typed-storage.types';
17
- export { BaseStorageAdapter, MemoryStorageAdapter, RedisStorageAdapter, createRedisClient, VercelKvStorageAdapter, UpstashStorageAdapter, FileSystemStorageAdapter, LocalStorageAdapter, IndexedDBStorageAdapter, } from './adapters';
18
- export type { CreateRedisClientOptions, FileSystemAdapterOptions, LocalStorageAdapterOptions, IndexedDBAdapterOptions, } from './adapters';
19
+ export { BaseStorageAdapter, MemoryStorageAdapter, RedisStorageAdapter, createRedisClient, VercelKvStorageAdapter, createVercelKvClient, UpstashStorageAdapter, FileSystemStorageAdapter, LocalStorageAdapter, IndexedDBStorageAdapter, } from './adapters';
20
+ export type { CreateRedisClientOptions, VercelKvConnection, FileSystemAdapterOptions, LocalStorageAdapterOptions, IndexedDBAdapterOptions, } from './adapters';
19
21
  export { globToRegex, matchesPattern, validatePattern, escapeGlob } from './utils/pattern';
20
22
  export { MAX_TTL_SECONDS, validateTTL, validateOptionalTTL, ttlToExpiresAt, expiresAtToTTL, isExpired, normalizeTTL, } from './utils/ttl';
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Connection fields that may be written beside a Redis `url`.
3
+ */
4
+ export interface RedisUrlSiblingFields {
5
+ host?: string;
6
+ port?: number;
7
+ password?: string;
8
+ db?: number;
9
+ tls?: boolean;
10
+ }
11
+ /**
12
+ * Outcome of combining a Redis URL with the fields written beside it.
13
+ */
14
+ export interface RedisUrlMerge {
15
+ /** Fields the URL leaves out that the sibling fields supply. */
16
+ fillIns: Pick<RedisUrlSiblingFields, 'port' | 'password' | 'db' | 'tls'>;
17
+ /** Sibling fields whose value contradicts the URL. */
18
+ conflicts: Array<keyof RedisUrlSiblingFields>;
19
+ }
20
+ /**
21
+ * The URL is the base: fields beside it fill in only what it leaves out (a
22
+ * port, a password, a database, TLS), and a field that contradicts it is a
23
+ * conflict the caller must reject (#768). Returns `undefined` for an
24
+ * unparseable URL, which the caller reports on its own terms.
25
+ */
26
+ export declare function mergeRedisUrlFields(rawUrl: string, fields: RedisUrlSiblingFields): RedisUrlMerge | undefined;
27
+ /** Human-readable reason for {@link RedisUrlMerge.conflicts}. */
28
+ export declare function describeRedisUrlConflicts(conflicts: RedisUrlMerge['conflicts']): string;
@@ -309,6 +309,31 @@ export interface MemoryAdapterOptions {
309
309
  */
310
310
  maxEntries?: number;
311
311
  }
312
+ /**
313
+ * Redis connection by host.
314
+ */
315
+ export interface RedisHostConnectionConfig {
316
+ host: string;
317
+ port?: number;
318
+ password?: string;
319
+ db?: number;
320
+ tls?: boolean;
321
+ /** Unset for a host config (a config built from optional settings may carry `url: undefined`). */
322
+ url?: undefined;
323
+ }
324
+ /**
325
+ * Redis connection by URL (`redis://` / `rediss://`).
326
+ */
327
+ export interface RedisUrlConnectionConfig extends Partial<Omit<RedisHostConnectionConfig, 'url'>> {
328
+ url: string;
329
+ }
330
+ /**
331
+ * Fields beside a top-level `url` (`{ url, config: { password } }`) that fill in what the URL
332
+ * leaves out. Without a `url` the adapter needs a `host` and rejects this shape.
333
+ */
334
+ export interface RedisUrlFillInConfig extends Partial<Omit<RedisHostConnectionConfig, 'url'>> {
335
+ url?: undefined;
336
+ }
312
337
  /**
313
338
  * Options for Redis storage adapter.
314
339
  */
@@ -319,19 +344,16 @@ export interface RedisAdapterOptions {
319
344
  */
320
345
  client?: unknown;
321
346
  /**
322
- * Redis connection configuration.
347
+ * Redis connection configuration — `host`, or a `url` like the top-level
348
+ * `redis` option. Beside a URL, the other fields only fill in what the URL
349
+ * leaves out; a contradicting field is rejected.
323
350
  * Mutually exclusive with `client`.
324
351
  */
325
- config?: {
326
- host: string;
327
- port?: number;
328
- password?: string;
329
- db?: number;
330
- tls?: boolean;
331
- };
352
+ config?: RedisHostConnectionConfig | RedisUrlConnectionConfig | RedisUrlFillInConfig;
332
353
  /**
333
354
  * Redis connection URI.
334
- * e.g., "redis://user:pass@host:6379/0"
355
+ * e.g., "redis://user:pass@host:6379/0". Fields in `config` fill in what it
356
+ * leaves out (port, password, db, tls).
335
357
  * Mutually exclusive with `client`.
336
358
  */
337
359
  url?: string;