Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 143 additions & 25 deletions DSL/CronManager/script/store_secrets_in_vault.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,129 @@
set -e # Exit on any error

# Configuration
VAULT_ADDR="${VAULT_ADDR:-http://vault:8200}"
VAULT_TOKEN_FILE="/agent/out/token"
# Use VAULT_AGENT_URL which points to vault-agent-cron proxy
# The agent automatically injects the authentication token
VAULT_ADDR="${VAULT_AGENT_URL:-http://vault-agent-cron:8203}"

# Decryption Configuration
PRIVATE_KEY_CACHE=""
PRIVATE_KEY_PATH="secret/data/encryption/private_key"

# Logging function
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}

# ============================================================================
# DECRYPTION FUNCTIONS (RSA-OAEP)
# ============================================================================

# Fetch private key from Vault
fetch_private_key() {
if [ -n "$PRIVATE_KEY_CACHE" ]; then
# Key already cached
return 0
fi

log "Fetching private key from Vault..."

# Convert path for KV v2 API
local api_path=$(echo "$PRIVATE_KEY_PATH" | sed 's|^secret/|secret/data/|')

# Fetch private key from Vault
local response=$(curl -s -w "HTTPSTATUS:%{http_code}" \
-X GET \
"$VAULT_ADDR/v1/$api_path")

local http_code=$(echo "$response" | grep -o "HTTPSTATUS:[0-9]*" | cut -d: -f2)
local body=$(echo "$response" | sed -E 's/HTTPSTATUS:[0-9]*$//')

if [[ "$http_code" -ne 200 ]]; then
log "ERROR: Failed to fetch private key from Vault (HTTP $http_code)"
log "Response: $body"
exit 1
fi

# Extract private key from JSON response
PRIVATE_KEY_CACHE=$(echo "$body" | grep -o '"key":"[^"]*"' | sed 's/"key":"//; s/"$//' | sed 's/\\n/\n/g')

if [ -z "$PRIVATE_KEY_CACHE" ]; then
log "ERROR: Private key is empty or could not be extracted"
exit 1
fi

log "Private key fetched and cached successfully"
}

# Decrypt RSA-OAEP encrypted value
# Input: Base64-encoded encrypted value
# Output: Plaintext value
decrypt_rsa_oaep() {
local encrypted_base64="$1"

if [ -z "$encrypted_base64" ]; then
log "ERROR: decrypt_rsa_oaep called with empty value"
exit 1
fi

# Ensure private key is fetched
fetch_private_key

# Create temporary files for decryption
local temp_dir=$(mktemp -d)
local private_key_file="$temp_dir/private_key.pem"
local encrypted_file="$temp_dir/encrypted.bin"
local decrypted_file="$temp_dir/decrypted.txt"

# Cleanup function
cleanup_temp_files() {
rm -rf "$temp_dir" 2>/dev/null || true
}

# Set trap to cleanup on exit
trap cleanup_temp_files EXIT

# Write private key to temp file
echo "$PRIVATE_KEY_CACHE" > "$private_key_file"

# Decode base64 and write to temp file
echo "$encrypted_base64" | base64 -d > "$encrypted_file" 2>/dev/null || {
log "ERROR: Failed to decode base64 encrypted value"
cleanup_temp_files
exit 1
}

# Decrypt using OpenSSL with RSA-OAEP padding
openssl pkeyutl -decrypt \
-inkey "$private_key_file" \
-in "$encrypted_file" \
-out "$decrypted_file" \
-pkeyopt rsa_padding_mode:oaep \
-pkeyopt rsa_oaep_md:sha256 \
-pkeyopt rsa_mgf1_md:sha256 2>/dev/null || {
log "ERROR: Decryption failed - invalid ciphertext or wrong key"
cleanup_temp_files
exit 1
}

# Read decrypted value
local decrypted_value=$(cat "$decrypted_file")

# Cleanup
cleanup_temp_files

if [ -z "$decrypted_value" ]; then
log "ERROR: Decrypted value is empty"
exit 1
fi

echo "$decrypted_value"
}

# ============================================================================
# END DECRYPTION FUNCTIONS
# ============================================================================

log "=== Starting Vault Secrets Storage ==="

# Debug: Print received parameters
Expand All @@ -22,20 +137,9 @@ log " connectionId: $connectionId"
log " llmPlatform: $llmPlatform"
log " llmModel: $llmModel"
log " deploymentEnvironment: $deploymentEnvironment"
log " Vault Address: $VAULT_ADDR"

# Read vault token
if [ ! -f "$VAULT_TOKEN_FILE" ]; then
log "ERROR: Vault token file not found at $VAULT_TOKEN_FILE"
exit 1
fi

VAULT_TOKEN=$(cat "$VAULT_TOKEN_FILE")
if [ -z "$VAULT_TOKEN" ]; then
log "ERROR: Vault token is empty"
exit 1
fi

log "Vault token loaded successfully"
# Note: No token required - vault agent proxy automatically injects authentication

# Function to determine platform name
get_platform_name() {
Expand Down Expand Up @@ -115,13 +219,17 @@ store_aws_llm_secrets() {

log "Storing AWS LLM secrets..."

# Decrypt sensitive fields
local decrypted_access_key=$(decrypt_rsa_oaep "$accessKey")
local decrypted_secret_key=$(decrypt_rsa_oaep "$secretKey")

# Build JSON payload
local json_payload=$(cat <<EOF
{
"data": {
"connection_id": "$connectionId",
"access_key": "$accessKey",
"secret_key": "$secretKey",
"access_key": "$decrypted_access_key",
"secret_key": "$decrypted_secret_key",
"environment": "$deploymentEnvironment",
"model": "$model",
"tags": "aws,bedrock,$deploymentEnvironment,$model"
Expand All @@ -137,9 +245,9 @@ EOF
log "API URL: $VAULT_ADDR/v1/$api_path"

# Execute HTTP API call
# No X-Vault-Token header needed - vault agent proxy auto-injects it
local response=$(curl -s -w "HTTPSTATUS:%{http_code}" \
-X POST \
-H "X-Vault-Token: $VAULT_TOKEN" \
-H "Content-Type: application/json" \
-d "$json_payload" \
"$VAULT_ADDR/v1/$api_path")
Expand All @@ -163,13 +271,16 @@ store_azure_llm_secrets() {

log "Storing Azure LLM secrets..."

# Decrypt sensitive fields
local decrypted_api_key=$(decrypt_rsa_oaep "$apiKey")

# Build JSON payload
local json_payload=$(cat <<EOF
{
"data": {
"connection_id": "$connectionId",
"endpoint": "$targetUrl",
"api_key": "$apiKey",
"api_key": "$decrypted_api_key",
"deployment_name": "$deploymentName",
"environment": "$deploymentEnvironment",
"model": "$model",
Expand All @@ -187,9 +298,9 @@ EOF
log "API URL: $VAULT_ADDR/v1/$api_path"

# Execute HTTP API call
# No X-Vault-Token header needed - vault agent proxy auto-injects it
local response=$(curl -s -w "HTTPSTATUS:%{http_code}" \
-X POST \
-H "X-Vault-Token: $VAULT_TOKEN" \
-H "Content-Type: application/json" \
-d "$json_payload" \
"$VAULT_ADDR/v1/$api_path")
Expand All @@ -212,13 +323,17 @@ store_aws_embedding_secrets() {

log "Storing AWS embedding secrets..."

# Decrypt sensitive fields
local decrypted_embedding_access_key=$(decrypt_rsa_oaep "$embeddingAccessKey")
local decrypted_embedding_secret_key=$(decrypt_rsa_oaep "$embeddingSecretKey")

# Build JSON payload
local json_payload=$(cat <<EOF
{
"data": {
"connection_id": "$connectionId",
"access_key": "$embeddingAccessKey",
"secret_key": "$embeddingSecretKey",
"access_key": "$decrypted_embedding_access_key",
"secret_key": "$decrypted_embedding_secret_key",
"environment": "$deploymentEnvironment",
"model": "$embeddingModel",
"tags": "aws,bedrock,embedding,$deploymentEnvironment,$embeddingModel"
Expand All @@ -234,9 +349,9 @@ EOF
log "API URL: $VAULT_ADDR/v1/$api_path"

# Execute HTTP API call
# No X-Vault-Token header needed - vault agent proxy auto-injects it
local response=$(curl -s -w "HTTPSTATUS:%{http_code}" \
-X POST \
-H "X-Vault-Token: $VAULT_TOKEN" \
-H "Content-Type: application/json" \
-d "$json_payload" \
"$VAULT_ADDR/v1/$api_path")
Expand All @@ -259,13 +374,16 @@ store_azure_embedding_secrets() {

log "Storing Azure embedding secrets..."

# Decrypt sensitive fields
local decrypted_embedding_api_key=$(decrypt_rsa_oaep "$embeddingAzureApiKey")

# Build JSON payload
local json_payload=$(cat <<EOF
{
"data": {
"connection_id": "$connectionId",
"endpoint": "$embeddingTargetUri",
"api_key": "$embeddingAzureApiKey",
"api_key": "$decrypted_embedding_api_key",
"deployment_name": "$embeddingDeploymentName",
"environment": "$deploymentEnvironment",
"model": "$embeddingModel",
Expand All @@ -283,9 +401,9 @@ EOF
log "API URL: $VAULT_ADDR/v1/$api_path"

# Execute HTTP API call
# No X-Vault-Token header needed - vault agent proxy auto-injects it
local response=$(curl -s -w "HTTPSTATUS:%{http_code}" \
-X POST \
-H "X-Vault-Token: $VAULT_TOKEN" \
-H "Content-Type: application/json" \
-d "$json_payload" \
"$VAULT_ADDR/v1/$api_path")
Expand Down
70 changes: 70 additions & 0 deletions GUI/vite.config.ts.timestamp-1767830818555-53ce0662a32c2.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// vite.config.ts
import { defineConfig } from "file:///app/node_modules/vite/dist/node/index.js";
import react from "file:///app/node_modules/@vitejs/plugin-react/dist/index.mjs";
import tsconfigPaths from "file:///app/node_modules/vite-tsconfig-paths/dist/index.mjs";
import svgr from "file:///app/node_modules/vite-plugin-svgr/dist/index.mjs";
import path from "path";

// vitePlugin.js
function removeHiddenMenuItems(str) {
var _a, _b;
const badJson = str.replace("export default [", "[").replace("];", "]");
const correctJson = badJson.replace(/(['"])?([a-z0-9A-Z_]+)(['"])?:/g, '"$2": ');
const isHiddenFeaturesEnabled = ((_a = process.env.REACT_APP_ENABLE_HIDDEN_FEATURES) == null ? void 0 : _a.toLowerCase().trim()) === "true" || ((_b = process.env.REACT_APP_ENABLE_HIDDEN_FEATURES) == null ? void 0 : _b.toLowerCase().trim()) === "1";
const json = removeHidden(JSON.parse(correctJson), isHiddenFeaturesEnabled);
const updatedJson = JSON.stringify(json);
return "export default " + updatedJson + ";";
}
function removeHidden(menuItems, isHiddenFeaturesEnabled) {
var _a;
if (!menuItems)
return menuItems;
const arr = (_a = menuItems == null ? void 0 : menuItems.filter((x) => !x.hidden)) == null ? void 0 : _a.filter((x) => isHiddenFeaturesEnabled || x.hiddenMode !== "production");
for (const a of arr) {
a.children = removeHidden(a.children, isHiddenFeaturesEnabled);
}
return arr;
}

// vite.config.ts
var __vite_injected_original_dirname = "/app";
var vite_config_default = defineConfig({
envPrefix: "REACT_APP_",
plugins: [
react(),
tsconfigPaths(),
svgr(),
{
name: "removeHiddenMenuItemsPlugin",
transform: (str, id) => {
if (!id.endsWith("/menu-structure.json"))
return str;
return removeHiddenMenuItems(str);
}
}
],
base: "/rag-search",
build: {
outDir: "./build",
target: "es2015",
emptyOutDir: true
},
server: {
headers: {
...process.env.REACT_APP_CSP && {
"Content-Security-Policy": process.env.REACT_APP_CSP
}
},
allowedHosts: ["est-rag-rtc.rootcode.software", "localhost", "127.0.0.1"]
},
resolve: {
alias: {
"~@fontsource": path.resolve(__vite_injected_original_dirname, "node_modules/@fontsource"),
"@": `${path.resolve(__vite_injected_original_dirname, "./src")}`
}
}
});
export {
vite_config_default as default
};
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiLCAidml0ZVBsdWdpbi5qcyJdLAogICJzb3VyY2VzQ29udGVudCI6IFsiY29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2Rpcm5hbWUgPSBcIi9hcHBcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9hcHAvdml0ZS5jb25maWcudHNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL2FwcC92aXRlLmNvbmZpZy50c1wiO2ltcG9ydCB7IGRlZmluZUNvbmZpZyB9IGZyb20gJ3ZpdGUnO1xuaW1wb3J0IHJlYWN0IGZyb20gJ0B2aXRlanMvcGx1Z2luLXJlYWN0JztcbmltcG9ydCB0c2NvbmZpZ1BhdGhzIGZyb20gJ3ZpdGUtdHNjb25maWctcGF0aHMnO1xuaW1wb3J0IHN2Z3IgZnJvbSAndml0ZS1wbHVnaW4tc3Zncic7XG5pbXBvcnQgcGF0aCBmcm9tICdwYXRoJztcbmltcG9ydCB7IHJlbW92ZUhpZGRlbk1lbnVJdGVtcyB9IGZyb20gJy4vdml0ZVBsdWdpbic7XG5cbi8vIGh0dHBzOi8vdml0ZWpzLmRldi9jb25maWcvXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICBlbnZQcmVmaXg6ICdSRUFDVF9BUFBfJyxcbiAgcGx1Z2luczogW1xuICAgIHJlYWN0KCksXG4gICAgdHNjb25maWdQYXRocygpLFxuICAgIHN2Z3IoKSxcbiAgICB7XG4gICAgICBuYW1lOiAncmVtb3ZlSGlkZGVuTWVudUl0ZW1zUGx1Z2luJyxcbiAgICAgIHRyYW5zZm9ybTogKHN0ciwgaWQpID0+IHtcbiAgICAgICAgaWYoIWlkLmVuZHNXaXRoKCcvbWVudS1zdHJ1Y3R1cmUuanNvbicpKVxuICAgICAgICAgIHJldHVybiBzdHI7XG4gICAgICAgIHJldHVybiByZW1vdmVIaWRkZW5NZW51SXRlbXMoc3RyKTtcbiAgICAgIH0sXG4gICAgfSxcbiAgXSxcbiAgYmFzZTogJy9yYWctc2VhcmNoJyxcbiAgYnVpbGQ6IHtcbiAgICBvdXREaXI6ICcuL2J1aWxkJyxcbiAgICB0YXJnZXQ6ICdlczIwMTUnLFxuICAgIGVtcHR5T3V0RGlyOiB0cnVlLFxuICB9LFxuICBzZXJ2ZXI6IHtcbiAgICBoZWFkZXJzOiB7XG4gICAgICAuLi4ocHJvY2Vzcy5lbnYuUkVBQ1RfQVBQX0NTUCAmJiB7XG4gICAgICAgICdDb250ZW50LVNlY3VyaXR5LVBvbGljeSc6IHByb2Nlc3MuZW52LlJFQUNUX0FQUF9DU1AsXG4gICAgICB9KSxcbiAgICB9LFxuICAgIGFsbG93ZWRIb3N0czogWydlc3QtcmFnLXJ0Yy5yb290Y29kZS5zb2Z0d2FyZScsICdsb2NhbGhvc3QnLCAnMTI3LjAuMC4xJ10sXG5cbiAgfSxcbiAgcmVzb2x2ZToge1xuICAgIGFsaWFzOiB7XG4gICAgICAnfkBmb250c291cmNlJzogcGF0aC5yZXNvbHZlKF9fZGlybmFtZSwgJ25vZGVfbW9kdWxlcy9AZm9udHNvdXJjZScpLFxuICAgICAgJ0AnOiBgJHtwYXRoLnJlc29sdmUoX19kaXJuYW1lLCAnLi9zcmMnKX1gLFxuICAgIH0sXG4gIH0sXG59KTtcbiIsICJjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSA9IFwiL2FwcFwiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL2FwcC92aXRlUGx1Z2luLmpzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9hcHAvdml0ZVBsdWdpbi5qc1wiO2V4cG9ydCBmdW5jdGlvbiByZW1vdmVIaWRkZW5NZW51SXRlbXMoc3RyKSB7XG4gIGNvbnN0IGJhZEpzb24gPSBzdHIucmVwbGFjZSgnZXhwb3J0IGRlZmF1bHQgWycsICdbJykucmVwbGFjZSgnXTsnLCAnXScpO1xuICBjb25zdCBjb3JyZWN0SnNvbiA9IGJhZEpzb24ucmVwbGFjZSgvKFsnXCJdKT8oW2EtejAtOUEtWl9dKykoWydcIl0pPzovZywgJ1wiJDJcIjogJyk7XG5cbiBjb25zdCBpc0hpZGRlbkZlYXR1cmVzRW5hYmxlZCA9IFxuICAgIHByb2Nlc3MuZW52LlJFQUNUX0FQUF9FTkFCTEVfSElEREVOX0ZFQVRVUkVTPy50b0xvd2VyQ2FzZSgpLnRyaW0oKSA9PT0gJ3RydWUnIHx8XG4gICAgcHJvY2Vzcy5lbnYuUkVBQ1RfQVBQX0VOQUJMRV9ISURERU5fRkVBVFVSRVM/LnRvTG93ZXJDYXNlKCkudHJpbSgpID09PSAnMSc7XG5cbiAgY29uc3QganNvbiA9IHJlbW92ZUhpZGRlbihKU09OLnBhcnNlKGNvcnJlY3RKc29uKSwgaXNIaWRkZW5GZWF0dXJlc0VuYWJsZWQpO1xuICBcbiAgY29uc3QgdXBkYXRlZEpzb24gPSBKU09OLnN0cmluZ2lmeShqc29uKTtcblxuICByZXR1cm4gJ2V4cG9ydCBkZWZhdWx0ICcgKyB1cGRhdGVkSnNvbiArICc7J1xufVxuXG5mdW5jdGlvbiByZW1vdmVIaWRkZW4obWVudUl0ZW1zLCBpc0hpZGRlbkZlYXR1cmVzRW5hYmxlZCkge1xuICBpZighbWVudUl0ZW1zKSByZXR1cm4gbWVudUl0ZW1zO1xuICBjb25zdCBhcnIgPSBtZW51SXRlbXNcbiAgICA/LmZpbHRlcih4ID0+ICF4LmhpZGRlbilcbiAgICA/LmZpbHRlcih4ID0+IGlzSGlkZGVuRmVhdHVyZXNFbmFibGVkIHx8IHguaGlkZGVuTW9kZSAhPT0gXCJwcm9kdWN0aW9uXCIpO1xuICBmb3IgKGNvbnN0IGEgb2YgYXJyKSB7XG4gICAgYS5jaGlsZHJlbiA9IHJlbW92ZUhpZGRlbihhLmNoaWxkcmVuLCBpc0hpZGRlbkZlYXR1cmVzRW5hYmxlZCk7XG4gIH1cbiAgcmV0dXJuIGFycjtcbn1cbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBOEwsU0FBUyxvQkFBb0I7QUFDM04sT0FBTyxXQUFXO0FBQ2xCLE9BQU8sbUJBQW1CO0FBQzFCLE9BQU8sVUFBVTtBQUNqQixPQUFPLFVBQVU7OztBQ0prTCxTQUFTLHNCQUFzQixLQUFLO0FBQXZPO0FBQ0UsUUFBTSxVQUFVLElBQUksUUFBUSxvQkFBb0IsR0FBRyxFQUFFLFFBQVEsTUFBTSxHQUFHO0FBQ3RFLFFBQU0sY0FBYyxRQUFRLFFBQVEsbUNBQW1DLFFBQVE7QUFFaEYsUUFBTSw0QkFDSCxhQUFRLElBQUkscUNBQVosbUJBQThDLGNBQWMsWUFBVyxZQUN2RSxhQUFRLElBQUkscUNBQVosbUJBQThDLGNBQWMsWUFBVztBQUV6RSxRQUFNLE9BQU8sYUFBYSxLQUFLLE1BQU0sV0FBVyxHQUFHLHVCQUF1QjtBQUUxRSxRQUFNLGNBQWMsS0FBSyxVQUFVLElBQUk7QUFFdkMsU0FBTyxvQkFBb0IsY0FBYztBQUMzQztBQUVBLFNBQVMsYUFBYSxXQUFXLHlCQUF5QjtBQWYxRDtBQWdCRSxNQUFHLENBQUM7QUFBVyxXQUFPO0FBQ3RCLFFBQU0sT0FBTSw0Q0FDUixPQUFPLE9BQUssQ0FBQyxFQUFFLFlBRFAsbUJBRVIsT0FBTyxPQUFLLDJCQUEyQixFQUFFLGVBQWU7QUFDNUQsYUFBVyxLQUFLLEtBQUs7QUFDbkIsTUFBRSxXQUFXLGFBQWEsRUFBRSxVQUFVLHVCQUF1QjtBQUFBLEVBQy9EO0FBQ0EsU0FBTztBQUNUOzs7QUR4QkEsSUFBTSxtQ0FBbUM7QUFRekMsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsV0FBVztBQUFBLEVBQ1gsU0FBUztBQUFBLElBQ1AsTUFBTTtBQUFBLElBQ04sY0FBYztBQUFBLElBQ2QsS0FBSztBQUFBLElBQ0w7QUFBQSxNQUNFLE1BQU07QUFBQSxNQUNOLFdBQVcsQ0FBQyxLQUFLLE9BQU87QUFDdEIsWUFBRyxDQUFDLEdBQUcsU0FBUyxzQkFBc0I7QUFDcEMsaUJBQU87QUFDVCxlQUFPLHNCQUFzQixHQUFHO0FBQUEsTUFDbEM7QUFBQSxJQUNGO0FBQUEsRUFDRjtBQUFBLEVBQ0EsTUFBTTtBQUFBLEVBQ04sT0FBTztBQUFBLElBQ0wsUUFBUTtBQUFBLElBQ1IsUUFBUTtBQUFBLElBQ1IsYUFBYTtBQUFBLEVBQ2Y7QUFBQSxFQUNBLFFBQVE7QUFBQSxJQUNOLFNBQVM7QUFBQSxNQUNQLEdBQUksUUFBUSxJQUFJLGlCQUFpQjtBQUFBLFFBQy9CLDJCQUEyQixRQUFRLElBQUk7QUFBQSxNQUN6QztBQUFBLElBQ0Y7QUFBQSxJQUNBLGNBQWMsQ0FBQyxpQ0FBaUMsYUFBYSxXQUFXO0FBQUEsRUFFMUU7QUFBQSxFQUNBLFNBQVM7QUFBQSxJQUNQLE9BQU87QUFBQSxNQUNMLGdCQUFnQixLQUFLLFFBQVEsa0NBQVcsMEJBQTBCO0FBQUEsTUFDbEUsS0FBSyxHQUFHLEtBQUssUUFBUSxrQ0FBVyxPQUFPLENBQUM7QUFBQSxJQUMxQztBQUFBLEVBQ0Y7QUFDRixDQUFDOyIsCiAgIm5hbWVzIjogW10KfQo=
Loading
Loading