File size: 13,377 Bytes
979853c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
# Validation helpers for interactive setup.

validate_uri() {
  local uri="$1"
  local db_type="$2"

  if [[ -z "$uri" ]]; then
    return 1
  fi

  case "$db_type" in
    postgresql)
      [[ "$uri" =~ ^postgres(ql)?://.+ ]]
      return $?; ;;
    neo4j)
      [[ "$uri" =~ ^(neo4j(\+s|\+ssc)?|bolt)://.+ ]]
      return $?; ;;
    mongodb)
      [[ "$uri" =~ ^mongodb(\+srv)?://.+ ]]
      return $?; ;;
    redis)
      [[ "$uri" =~ ^rediss?://.+ ]]
      return $?; ;;
    milvus|qdrant)
      [[ "$uri" =~ ^https?://.+ ]]
      return $?; ;;
    memgraph)
      [[ "$uri" =~ ^bolt://.+ ]]
      return $?; ;;
    *)
      return 1
      ;;
  esac
}

validate_api_key() {
  local key="$1"
  local provider="$2"

  if [[ -z "$key" ]]; then
    return 1
  fi

  case "$provider" in
    openai|openrouter)
      return 0; ;;
    *)
      [[ ${#key} -ge 8 ]]
      return $?; ;;
  esac
}

validate_port() {
  local port="$1"

  if [[ ! "$port" =~ ^[0-9]+$ ]]; then
    return 1
  fi

  if (( port < 1 || port > 65535 )); then
    return 1
  fi

  return 0
}

validate_positive_integer() {
  local value="$1"

  if [[ ! "$value" =~ ^[0-9]+$ ]]; then
    return 1
  fi

  (( 10#$value > 0 ))
}

validate_non_negative_integer() {
  local value="$1"

  if [[ ! "$value" =~ ^[0-9]+$ ]]; then
    return 1
  fi

  (( 10#$value >= 0 ))
}

validate_non_empty() {
  local value="$1"

  [[ -n "$value" ]]
}

validate_existing_file() {
  local path="$1"

  [[ -n "$path" && -f "$path" ]]
}

check_storage_compatibility() {
  local kv_storage="$1"
  local vector_storage="$2"
  local graph_storage="$3"
  local doc_status_storage="$4"
  local warnings=()

  if [[ "$vector_storage" == "MongoVectorDBStorage" ]]; then
    warnings+=("MongoDB vector storage requires an Atlas-capable deployment with Atlas Search / Vector Search support.")
  fi

  if [[ "$graph_storage" == "Neo4JStorage" && "$kv_storage" == "JsonKVStorage" ]]; then
    warnings+=("Neo4j graph with JSON KV storage is fine for dev, but not ideal for production.")
  fi

  if [[ "$graph_storage" == "NetworkXStorage" ]]; then
    warnings+=("NetworkX graph storage is memory-bound and suited for small datasets only.")
  fi

  if [[ "$vector_storage" == "FaissVectorDBStorage" ]]; then
    warnings+=("Faiss vector storage is local-only and requires manual persistence management.")
  fi

  if [[ "$kv_storage" == "JsonKVStorage" || "$doc_status_storage" == "JsonDocStatusStorage" ]]; then
    warnings+=("JSON-based KV/doc status storage is recommended only for local development.")
  fi

  if ((${#warnings[@]} > 0)); then
    echo "${COLOR_YELLOW:-}Storage compatibility/performance warnings:${COLOR_RESET:-}" >&2
    for warning in "${warnings[@]}"; do
      echo "  - $warning" >&2
    done
  fi

  return 0
}

format_error() {
  local message="$1"
  local suggestion="${2:-}"

  echo "${COLOR_RED:-}Error:${COLOR_RESET:-} $message" >&2
  if [[ -n "$suggestion" ]]; then
    echo "${COLOR_YELLOW:-}Hint:${COLOR_RESET:-} $suggestion" >&2
  fi
}

contains_env_interpolation_syntax() {
  local value="$1"

  [[ "$value" == *'${'* ]]
}

is_sensitive_env_key() {
  local key="$1"

  case "$key" in
    AUTH_ACCOUNTS|*API_KEY*|*ACCESS_KEY*|*PUBLIC_KEY*|*SECRET*|*PASSWORD*|*TOKEN*)
      return 0
      ;;
    *)
      return 1
      ;;
  esac
}

validate_sensitive_env_literals() {
  local key value
  local invalid_keys=()

  for key in "${!ENV_VALUES[@]}"; do
    if ! is_sensitive_env_key "$key"; then
      continue
    fi

    value="${ENV_VALUES[$key]:-}"
    if [[ -n "$value" ]] && contains_env_interpolation_syntax "$value"; then
      invalid_keys+=("$key")
    fi
  done

  if ((${#invalid_keys[@]} > 0)); then
    format_error \
      "Sensitive values must not contain \${...} interpolation syntax: ${invalid_keys[*]}" \
      "Use literal values, plain \$ characters, or inject those secrets via runtime environment variables instead of .env."
    return 1
  fi

  return 0
}

validate_required_variables() {
  local storages=("$@")
  local missing=()
  local unknown=()
  local storage required var

  for storage in "${storages[@]}"; do
    if [[ -z "$storage" ]]; then
      continue
    fi
    if [[ ! -v "STORAGE_ENV_REQUIREMENTS[$storage]" ]]; then
      unknown+=("$storage")
      continue
    fi
    required="${STORAGE_ENV_REQUIREMENTS[$storage]}"
    if [[ -z "$required" ]]; then
      continue
    fi
    for var in $required; do
      if [[ -z "${ENV_VALUES[$var]:-}" ]]; then
        missing+=("$var")
      fi
    done
  done

  if ((${#unknown[@]} > 0)); then
    format_error \
      "Unsupported storage selections: ${unknown[*]}" \
      "Use a supported LightRAG storage class name or rerun setup to pick a valid backend."
    return 1
  fi

  if ((${#missing[@]} > 0)); then
    format_error "Missing required variables: ${missing[*]}" "Fill them in .env or re-run setup."
    return 1
  fi

  return 0
}

validate_opensearch_hosts_format() {
  local hosts="${1:-${ENV_VALUES[OPENSEARCH_HOSTS]:-}}"
  local entry=""
  local trimmed=""
  local has_host="no"
  local -a entries=()

  if [[ "$hosts" == *"://"* ]]; then
    format_error \
      "OPENSEARCH_HOSTS must use bare host:port entries, not URLs." \
      "Set comma-separated host:port values such as localhost:9200; control TLS with OPENSEARCH_USE_SSL."
    return 1
  fi

  IFS=',' read -r -a entries <<< "$hosts"
  for entry in "${entries[@]}"; do
    trimmed="${entry#"${entry%%[![:space:]]*}"}"
    trimmed="${trimmed%"${trimmed##*[![:space:]]}"}"
    if [[ -z "$trimmed" ]]; then
      format_error \
        "OPENSEARCH_HOSTS must not contain empty host entries." \
        "Use comma-separated host:port values such as localhost:9200 or host1:9200,host2:9200."
      return 1
    fi
    has_host="yes"
  done

  if [[ "$has_host" != "yes" ]]; then
    format_error \
      "OPENSEARCH_HOSTS must include at least one host:port entry." \
      "Set it to a value such as localhost:9200."
    return 1
  fi

  return 0
}

validate_opensearch_password_strength() {
  local password="${1:-${ENV_VALUES[OPENSEARCH_PASSWORD]:-}}"

  if [[ ${#password} -lt 8 || ! "$password" =~ [A-Z] || ! "$password" =~ [a-z] || ! "$password" =~ [0-9] || ! "$password" =~ [^A-Za-z0-9] ]]; then
    format_error \
      "OpenSearch requires a strong OPENSEARCH_PASSWORD." \
      "Use at least 8 characters with uppercase, lowercase, number, and special character."
    return 1
  fi

  return 0
}

validate_opensearch_config() {
  local deployment_mode="${1:-${ENV_VALUES[LIGHTRAG_SETUP_OPENSEARCH_DEPLOYMENT]:-}}"
  local hosts="${2:-${ENV_VALUES[OPENSEARCH_HOSTS]:-}}"
  local user="${3:-${ENV_VALUES[OPENSEARCH_USER]:-}}"
  local password="${4:-${ENV_VALUES[OPENSEARCH_PASSWORD]:-}}"
  local num_shards="${5-${ENV_VALUES[OPENSEARCH_NUMBER_OF_SHARDS]-1}}"
  local num_replicas="${6-${ENV_VALUES[OPENSEARCH_NUMBER_OF_REPLICAS]-0}}"

  if ! validate_opensearch_hosts_format "$hosts"; then
    return 1
  fi

  if [[ -z "$user" || -z "$password" ]]; then
    if [[ "$deployment_mode" == "docker" ]]; then
      format_error \
        "Bundled OpenSearch requires OPENSEARCH_USER and OPENSEARCH_PASSWORD." \
        "Set both variables or rerun setup; the managed Docker service starts with security enabled."
    else
      format_error \
        "OpenSearch requires both OPENSEARCH_USER and OPENSEARCH_PASSWORD." \
        "This setup wizard only supports authenticated OpenSearch clusters. Set both values or rerun setup."
    fi
    return 1
  fi

  if ! validate_opensearch_password_strength "$password"; then
    if [[ "$deployment_mode" == "docker" ]]; then
      echo "${COLOR_YELLOW:-}Hint:${COLOR_RESET:-} The managed Docker image also enforces this password strength at startup." >&2
    fi
    return 1
  fi

  if ! validate_positive_integer "$num_shards"; then
    format_error \
      "OPENSEARCH_NUMBER_OF_SHARDS must be a positive integer." \
      "Set it to 1 or greater, or rerun setup to regenerate the OpenSearch index settings."
    return 1
  fi

  if ! validate_non_negative_integer "$num_replicas"; then
    format_error \
      "OPENSEARCH_NUMBER_OF_REPLICAS must be a non-negative integer." \
      "Set it to 0 or greater, or rerun setup to regenerate the OpenSearch index settings."
    return 1
  fi

  return 0
}

validate_mongo_vector_storage_config() {
  local vector_storage="$1"
  local mongo_uri="${2:-${ENV_VALUES[MONGO_URI]:-}}"
  local mongo_deployment="${3:-${ENV_VALUES[LIGHTRAG_SETUP_MONGODB_DEPLOYMENT]:-}}"

  if [[ "$vector_storage" != "MongoVectorDBStorage" ]]; then
    return 0
  fi

  if [[ "$mongo_deployment" == "docker" ]]; then
    format_error \
      "MongoVectorDBStorage cannot use the local Docker MongoDB service managed by this setup wizard." \
      "That service is MongoDB Community Edition without Atlas Search / Vector Search support. Use an Atlas-capable MongoDB endpoint instead."
    return 1
  fi

  if ! validate_uri "$mongo_uri" mongodb; then
    format_error \
      "MongoVectorDBStorage requires a valid MongoDB URI." \
      "Set MONGO_URI to a mongodb:// or mongodb+srv:// endpoint that supports Atlas Search / Vector Search."
    return 1
  fi

  if [[ ! "$mongo_uri" =~ ^mongodb\+srv:// ]]; then
    format_error \
      "MongoVectorDBStorage requires a MongoDB Atlas URI." \
      "Set MONGO_URI to a mongodb+srv:// endpoint backed by Atlas Search / Vector Search."
    return 1
  fi

  return 0
}

validate_auth_accounts_format() {
  local auth_accounts="$1"
  local entry username password

  if [[ -z "$auth_accounts" ]]; then
    return 0
  fi

  if [[ "$auth_accounts" == ,* || "$auth_accounts" == *, || "$auth_accounts" == *",,"* ]]; then
    return 1
  fi

  IFS=',' read -r -a entries <<< "$auth_accounts"
  for entry in "${entries[@]}"; do
    if [[ -z "$entry" || "$entry" != *:* ]]; then
      return 1
    fi

    username="${entry%%:*}"
    password="${entry#*:}"
    if [[ -z "$username" || -z "$password" ]]; then
      return 1
    fi
  done

  return 0
}

validate_auth_accounts_password_safety() {
  local auth_accounts="$1"
  local entry password normalized_password

  if [[ -z "$auth_accounts" ]]; then
    return 0
  fi

  IFS=',' read -r -a entries <<< "$auth_accounts"
  for entry in "${entries[@]}"; do
    password="${entry#*:}"
    normalized_password="${password,,}"
    if [[ "$normalized_password" == admin* || "$normalized_password" == pass* ]]; then
      return 1
    fi
  done

  return 0
}

validate_auth_accounts_runtime_config() {
  local auth_accounts="$1"

  if [[ -z "$auth_accounts" ]]; then
    return 0
  fi

  if ! validate_auth_accounts_format "$auth_accounts"; then
    format_error \
      "AUTH_ACCOUNTS must use comma-separated user:password pairs." \
      "Use entries like admin:{bcrypt}<hash> or admin:secret,reader:another-secret."
    return 1
  fi

  return 0
}

whitelist_exposes_api_routes() {
  local whitelist_paths="$1"
  local entry trimmed_entry normalized_entry

  IFS=',' read -r -a entries <<< "$whitelist_paths"
  for entry in "${entries[@]}"; do
    trimmed_entry="${entry#"${entry%%[![:space:]]*}"}"
    trimmed_entry="${trimmed_entry%"${trimmed_entry##*[![:space:]]}"}"
    normalized_entry="$trimmed_entry"

    if [[ "$normalized_entry" == *"/*" ]]; then
      normalized_entry="${normalized_entry%/*}"
    fi
    if [[ "$normalized_entry" != "/" ]]; then
      normalized_entry="${normalized_entry%/}"
    fi

    if [[ "$normalized_entry" == "/api" || "$normalized_entry" == /api/* ]]; then
      return 0
    fi
  done

  return 1
}

validate_security_config() {
  local auth_accounts="${1:-${ENV_VALUES[AUTH_ACCOUNTS]:-}}"
  local token_secret="${2:-${ENV_VALUES[TOKEN_SECRET]:-}}"
  local _api_key="${3:-${ENV_VALUES[LIGHTRAG_API_KEY]:-}}"
  local _unused_flag="${4:-no}"
  local _unused_whitelist="${5:-${ENV_VALUES[WHITELIST_PATHS]:-}}"
  local _unused_whitelist_is_set="${6:-}"

  if [[ -z "$auth_accounts" ]]; then
    return 0
  fi

  if ! validate_auth_accounts_format "$auth_accounts"; then
    format_error \
      "AUTH_ACCOUNTS must use comma-separated user:password pairs." \
      "Use entries like admin:{bcrypt}<hash> or admin:secret,reader:another-secret."
    return 1
  fi

  if ! validate_auth_accounts_password_safety "$auth_accounts"; then
    format_error \
      "AUTH_ACCOUNTS passwords must not start with 'admin' or 'pass'." \
      "Choose a less predictable password or use lightrag-hash-password to generate a {bcrypt} value."
    return 1
  fi

  if [[ -z "$token_secret" ]]; then
    format_error \
      "AUTH_ACCOUNTS is set but TOKEN_SECRET is missing." \
      "Set a non-empty JWT signing secret before enabling account-based authentication."
    return 1
  fi

  if [[ "$token_secret" == "lightrag-jwt-default-secret-key!" ]]; then
    format_error \
      "TOKEN_SECRET must not use the built-in default value when AUTH_ACCOUNTS is enabled." \
      "Generate a unique JWT signing secret and update TOKEN_SECRET."
    return 1
  fi

  return 0
}

check_docker_availability() {
  if ! command -v docker >/dev/null 2>&1; then
    format_error "Docker is not installed or not in PATH." "Install Docker or disable docker service generation."
    return 1
  fi

  if ! docker compose version >/dev/null 2>&1; then
    format_error "Docker Compose is not available." "Install the Docker Compose plugin or use docker-compose."
    return 1
  fi

  return 0
}