updated readme file and added support for file based record from keeper

This commit is contained in:
Hitesh Borase
2026-01-14 11:01:12 +05:30
parent 19d87f527a
commit 65858f46b4
3 changed files with 184 additions and 374 deletions
+32
View File
@@ -0,0 +1,32 @@
# Dependencies - we install fresh in Docker
node_modules
npm-debug.log
yarn-error.log
# Git files - not needed in image
.git
.gitignore
.gitattributes
# IDE and editor files
.vscode
.idea
*.swp
*.swo
*~
.DS_Store
# Environment files
.env
.env.local
.env.*.local
# Build artifacts
dist/
build/
*.log
# Temporary files
tmp/
temp/
*.tmp
+118 -363
View File
@@ -1,139 +1,51 @@
# Harness Keeper Security Plugin
# Harness CI Keeper Plugin
A Harness CI/CD plugin that integrates with Keeper Security Secrets Manager (KSM) to securely fetch and inject secrets into your CI/CD pipelines at runtime.
Keeper Secrets Manager integration into Harness CI for dynamic secrets retrieval
## Overview
This plugin securely retrieves secrets from Keeper Security Secrets Manager and makes them available to subsequent steps in your Harness pipeline. The plugin implements a zero-knowledge architecture where secrets are retrieved directly from Keeper Vault at runtime and never pass through Harness systems in decrypted form.
**Key Features:**
- **Zero-Knowledge Architecture**: Secrets are retrieved directly from Keeper Vault at runtime
- **Multiple Authentication Methods**: Supports one-time access tokens (`US:...`) and full JSON config
- **Keeper Notation Support**: Query fields (`field`), custom fields (`custom_field`), and file attachments (`file`)
- **Multiple Output Formats**: Environment variables, file output, and output variables
- **Secure Storage**: Secrets written to `/harness/secrets/` with secure file permissions
- **Automatic Validation**: Validates token format and required config fields
- **Base64 Decoding**: Automatically decodes base64-encoded tokens
## Features
## Architecture
### Zero-Knowledge Design
The plugin ensures zero-knowledge security through the following design:
1. **Harness CI/CD** calls the custom Docker-based plugin
2. **Plugin authenticates** using KSM configuration stored in Harness Secrets Manager
3. **Secrets are retrieved** directly from Keeper Vault at runtime
4. **Decrypted secrets** are returned as step outputs for downstream stages
5. **No external storage**: Secrets never pass through Harness systems in decrypted form
- Retrieve secrets from the Keeper Vault within the Harness CI pipeline
- Support for standard fields, custom fields, and file attachments
- Zero-knowledge architecture - secrets retrieved directly from Keeper at runtime
All secret flow happens strictly between the plugin container and Keeper, while Harness provides orchestration, governance, and secure secret injection across the pipeline.
### Components
1. **`src/index.js`**: Main plugin logic
- Token processing and validation
- Keeper Secrets Manager integration
- Secret fetching using Keeper Notation
- Secret distribution
2. **`entrypoint.sh`**: Container entrypoint script
- Executes the Node.js plugin
- Captures and processes plugin output
- Writes secrets to Harness directories
- Manages security and cleanup
3. **`Example/Pipeline.yaml`**: Example Harness CI pipeline
- Demonstrates plugin configuration
- Shows secret consumption patterns
### Execution Flow
```
1. Container starts → entrypoint.sh executes
2. entrypoint.sh → Runs: node /app/src/index.js
3. Plugin processes:
- Reads KSM_CONFIG from environment
- Validates and decodes token (base64 if needed)
- Initializes Keeper storage
- Fetches secrets from Keeper using Keeper Notation
- Outputs secrets to stdout (with ENV:/OUT: prefixes)
4. entrypoint.sh captures stdout:
- Parses ENV:/OUT: prefixes
- Writes to /harness/outputs/outputs.txt (output variables)
- Writes to /harness/secrets/ (file-based access)
- Sets secure permissions (chmod 600)
5. Subsequent steps access secrets via:
- Files: /harness/secrets/VARIABLE_NAME (recommended)
```
## Prerequisites
- **Keeper Secrets Manager access** ([Quick Start Guide](https://docs.keeper.io/secrets-manager/secrets-manager/quick-start-guide))
- **Secrets Manager addon enabled** for your Keeper account
- **Membership in a Role** with the Secrets Manager enforcement policy enabled
- **A Keeper Secrets Manager Application** with secrets shared to it
- **An initialized Keeper Secrets Manager Configuration** (JSON or one-time token)
- **Record UID(s)** for the secrets you want to access ⚠️ **REQUIRED**
- **Harness CI/CD account** with a project set up
- **Docker** (for building the plugin image)
- **Node.js 18+** (for local development)
- Keeper Secrets Manager access with Application configured
- Harness CI account with project setup
- KSM configuration (one-time access token `US:...` or Base64-encoded token or JSON confign)
## Installation
## About
### Build and Push Docker Image
The plugin retrieves secrets from Keeper Secrets Manager and stores them in **Harness pipeline volume storage** at `/harness/secrets/` directory for reliable file-based access by subsequent steps.
Build the plugin Docker image:
**Important:** Secrets are stored in `/harness/secrets/` directory (Harness shared workspace volume), not as Harness environment variables.
```bash
docker build -t your-dockerhub-username/harness-keeper-plugin:latest .
All secrets are:
- **Pipeline-scoped**: Only accessible during the current pipeline execution
- **Automatically cleaned up**: Harness CI removes all secrets after pipeline completion
- **Securely stored**: Files have restricted permissions (600 - owner read/write only)
### Project Structure
```
harness_keeper_plugin/
├── src/
│ └── index.js # Main plugin logic
├── entrypoint.sh # Container entrypoint script
├── Dockerfile # Docker image definition
├── package.json # Node.js dependencies
├── Example/
│ └── Pipeline.yaml # Example Harness CI pipeline
└── README.md # This file
```
Push to your container registry:
```bash
docker push your-dockerhub-username/harness-keeper-plugin:latest
```
**Note:** Replace `your-dockerhub-username` with your actual Docker Hub username or container registry path.
## Configuration
### 1. Create Keeper Access Token
#### Option A: One-Time Access Token
1. Log in to Keeper Security
2. Navigate to Secrets Manager
3. Generate a one-time access token (format: `US:xxxxx`)
4. Base64 encode (optional but recommended):
```bash
echo -n "US:your-token-here" | base64
```
#### Option B: Full JSON Config
Create a JSON config with:
```json
{
"hostname": "keepersecurity.com",
"clientId": "your-client-id",
"privateKey": "your-private-key"
}
```
Base64 encode:
```bash
echo -n '{"hostname":"...","clientId":"...","privateKey":"..."}' | base64
```
### 2. Create Harness Secret
In Harness, create a secret:
- **Type**: TEXT or FILE (both work)
- **Name**: `KSM_CONFIG` (or your preferred name)
- **Value**: Your base64-encoded token or JSON config
### 3. Configure Pipeline
**Before you begin:** You must have the Record UID(s) for the secrets you want to access. See "How to Get Record UID" section below.
## Quick Start
```yaml
pipeline:
@@ -161,15 +73,15 @@ pipeline:
name: Fetch_Keeper_Secrets
identifier: Fetch_Keeper_Secrets
spec:
image: your-dockerhub-username/harness-keeper-plugin:latest
image: dhborse/keeper-harness-plugin
settings:
secrets: |
JQPso5SMBpP29gKKGp6Enw/field/password > env:DB_PASSWORD
JQPso5SMBpP29gKKGp6Enw/field/login > env:DB_USERNAME
JQPso5SMBpP29gKKGp6Enw/custom_field/API_Key > env:API_KEY
JQPso5SMBpP29gKKGp6Enw/file/api.key > file:/app/config/api.key
VeYTRo-PHElAwfQT6f0TIA/field/password > DB_PASSWORD
VeYTRo-PHElAwfQT6f0TIA/field/login > DB_USERNAME
VeYTRo-PHElAwfQT6f0TIA/custom_field/text > KEEPER_TEXT
VeYTRo-PHElAwfQT6f0TIA/file/credentials.txt > FILE_DATA
envVariables:
KSM_CONFIG: <+secrets.getValue("KSM_CONFIG")>
KSM_CONFIG: <+secrets.getValue("Test_File_secret")>
- step:
type: Run
name: Use_Keeper_Secrets
@@ -178,116 +90,79 @@ pipeline:
image: alpine:3.20
shell: Sh
command: |
if [ -f /harness/secrets/DB_USERNAME ] && [ -f /harness/secrets/DB_PASSWORD ]; then
DB_USERNAME=$(cat /harness/secrets/DB_USERNAME)
DB_PASSWORD=$(cat /harness/secrets/DB_PASSWORD)
API_KEY=$(cat /harness/secrets/API_KEY)
echo "Secrets retrieved successfully"
# Use secrets in your build/deploy process
echo "Username: $DB_USERNAME"
echo "Password: $DB_PASSWORD"
fi
if [ -f /harness/secrets/FILE_DATA ]; then
FILE_DATA=$(cat /harness/secrets/FILE_DATA)
echo "File Data: $FILE_DATA"
fi
```
**Note:** `envVariables` is used ONLY to pass the Keeper configuration token to the plugin. The actual secrets retrieved from Keeper are stored in Harness pipeline volume storage at `/harness/secrets/`.
## Inputs
## Usage
### KSM_CONFIG
### Secret Mapping Format
Keeper Secrets Manager configuration for authentication. Store in Harness secrets and reference:
```
<keeper-notation> > <destination>
```yaml
envVariables:
KSM_CONFIG: <+secrets.getValue("Keeper_Config_Secret")>
```
### Keeper Notation Format
**Supported Formats:**
- One-time access token: `US:xxxxx`
- JSON configuration: `{"hostname": "...", "clientId": "...", "privateKey": "..."}`
- Base64-encoded token or JSON config
### Secrets
Keeper Notation queries mapping secrets to destinations:
**Format:** `<keeper-notation> > <destination>`
**Example:**
```yaml
secrets: |
VeYTRo-PHElAwfQT6f0TIA/field/password > DB_PASSWORD
VeYTRo-PHElAwfQT6f0TIA/field/login > DB_USERNAME
VeYTRo-PHElAwfQT6f0TIA/custom_field/text > KEEPER_TEXT
VeYTRo-PHElAwfQT6f0TIA/file/credentials.txt > FILE_DATA
```
## Keeper Notation Format
The plugin supports three types of Keeper Notation queries:
1. **Standard Fields**: `<record-uid>/field/<field-type>`
- Examples: `password`, `login`, `url`, `text`, `host`, etc.
| Type | Format | Example |
|------|--------|---------|
| **Standard Fields** | `<record-uid>/field/<field-type>` | `VeYTRo.../field/password` |
| **Custom Fields** | `<record-uid>/custom_field/<field-label>` | `VeYTRo.../custom_field/API_Key` |
| **File Attachments** | `<record-uid>/file/<file-name>` | `VeYTRo.../file/credentials.txt` |
2. **Custom Fields**: `<record-uid>/custom_field/<field-label>`
- Examples: `API_Key`, `My Custom Field`, `Environment`, etc.
**Note:** Record UID is the unique identifier for a secret record in Keeper. Get it from Keeper Vault → Record details → Record UID.
3. **File Attachments**: `<record-uid>/file/<file-name>`
- Examples: `api.key`, `certificate.pem`, `config.json`, etc.
## Destination Format
**Important:** The first part (e.g., `JQPso5SMBpP29gKKGp6Enw`) is the **Record UID**, the unique identifier for a specific secret record in Keeper.
The destination defines where the secret is stored:
### Destination Types
| Format | Description | Output Location |
|--------|-------------|----------------|
| `VARIABLE_NAME` | Default output (recommended) | `/harness/secrets/VARIABLE_NAME` |
| Destination Prefix | Description | Output Location |
|-------------------|-------------|----------------|
| `env:VARIABLE_NAME` | Environment variable | `/harness/secrets/VARIABLE_NAME` |
| `file:/path/to/file` | File output | Specified file path |
| `VARIABLE_NAME` (default) | Output variable | `/harness/secrets/VARIABLE_NAME` |
### Examples
#### 1. Standard Field → Environment Variable
**Examples:**
```yaml
secrets: |
JQPso5SMBpP29gKKGp6Enw/field/password > env:DB_PASSWORD
JQPso5SMBpP29gKKGp6Enw/field/login > env:DB_USERNAME
```
- Creates files: `/harness/secrets/DB_PASSWORD`, `/harness/secrets/DB_USERNAME`
- Access: `DB_PASSWORD=$(cat /harness/secrets/DB_PASSWORD)`
#### 2. Custom Field → Output Variable
```yaml
secrets: |
JQPso5SMBpP29gKKGp6Enw/custom_field/API_Key > API_KEY
JQPso5SMBpP29gKKGp6Enw/custom_field/Environment > ENV_NAME
```
- Creates files: `/harness/secrets/API_KEY`, `/harness/secrets/ENV_NAME`
- Access: `API_KEY=$(cat /harness/secrets/API_KEY)`
#### 3. File Attachment → File Path
```yaml
secrets: |
JQPso5SMBpP29gKKGp6Enw/file/api.key > file:/app/config/api.key
JQPso5SMBpP29gKKGp6Enw/file/certificate.pem > file:/app/ssl/cert.pem
```
- Downloads files and writes to specified paths
- Access: `cat /app/config/api.key`
#### 4. Mixed Usage
```yaml
secrets: |
JQPso5SMBpP29gKKGp6Enw/field/password > env:DB_PASSWORD
JQPso5SMBpP29gKKGp6Enw/custom_field/API_Key > API_KEY
JQPso5SMBpP29gKKGp6Enw/file/config.json > file:/app/config.json
# Default: saves to /harness/secrets/DB_PASSWORD
VeYTRo-PHElAwfQT6f0TIA/field/password > DB_PASSWORD
```
### How to Get Record UID
## Accessing Secrets
The Record UID is **mandatory** and identifies which specific secret record to retrieve. Obtain it using one of these methods:
**Method 1: From Keeper Web Vault**
1. Log in to your Keeper Vault
2. Open the secret record you want to access
3. Click on the record details/info icon
4. The Record UID is displayed in the record information (usually at the bottom)
5. Copy the Record UID (format: `JQPso5SMBpP29gKKGp6Enw`)
**Method 2: Using Keeper Commander CLI**
```bash
keeper list
# This will show all records with their UIDs
```
**Method 3: Using Keeper Secrets Manager SDK/API**
- Use the Keeper Secrets Manager SDK to list records and retrieve their UIDs
- The Record UID is returned when querying records
**Method 4: From Keeper Admin Console**
1. Navigate to Secrets Manager in Admin Console
2. View the Application and its shared records
3. Record UIDs are visible in the record list
**Note:** The one-time access token (`US:...`) or JSON config is used to **authenticate** and access Keeper, but you still need the **Record UID** to specify which secret record to retrieve.
## Accessing Secrets in Subsequent Steps
### Method 1: File-Based Access (Recommended)
The most reliable method for accessing secrets in subsequent steps:
Secrets are stored in `/harness/secrets/` directory. Read them in subsequent steps:
```yaml
- step:
@@ -297,177 +172,52 @@ The most reliable method for accessing secrets in subsequent steps:
image: alpine:3.20
shell: Sh
command: |
DB_PASSWORD=$(cat /harness/secrets/DB_PASSWORD)
DB_USERNAME=$(cat /harness/secrets/DB_USERNAME)
API_KEY=$(cat /harness/secrets/API_KEY)
# Use secrets in your process
echo "Secrets loaded successfully"
# Clean up after use (optional)
rm -f /harness/secrets/DB_PASSWORD /harness/secrets/DB_USERNAME
DB_PASSWORD=$(cat /harness/secrets/DB_PASSWORD)
FILE_DATA=$(cat /harness/secrets/FILE_DATA)
# Use secrets in your build/deploy process
```
**Benefits:**
- Most reliable method
- No truncation issues
- Works with all secret types
- Works with all secret types including binary files
- Supports long values and special characters
### Method 2: Direct File Access (for file: prefix)
## Secret Storage
When using `file:` prefix, files are written directly to the specified path:
```yaml
- step:
type: Run
name: Use_File
spec:
command: |
# File already at /app/config/api.key
cat /app/config/api.key
```
## Secret Storage Mechanism
**Important:** This plugin uses **Harness pipeline volume storage**, not Harness environment variables.
- **Storage Location:** `/harness/secrets/` directory (Harness shared workspace volume)
- **Storage Type:** File-based storage on pipeline-scoped volume
- **Access Method:** Read files directly using `cat` or file operations
- **Location:** `/harness/secrets/` directory (Harness shared workspace volume)
- **Permissions:** `600` (owner read/write only)
- **Scope:** Pipeline execution only - automatically cleaned up after completion
- **Permissions:** Files have `600` permissions (owner read/write only)
- **Access:** Read files directly using `cat` or file operations
The `envVariables` section in the plugin step is **only** used to pass the `KSM_CONFIG` (Keeper configuration token) to the plugin. The actual secrets retrieved from Keeper are written to files in `/harness/secrets/` directory.
The `envVariables` section is **only** used to pass `KSM_CONFIG` to the plugin. Actual secrets are written to files in `/harness/secrets/` directory.
## Security
### Zero-Knowledge Architecture
- **Direct Retrieval**: Secrets are retrieved directly from Keeper Vault at runtime
- **Zero-Knowledge**: Secrets retrieved directly from Keeper Vault at runtime
- **No Decrypted Storage**: Secrets never pass through Harness systems in decrypted form
- **Pipeline-Scoped**: All secrets are scoped to the current pipeline execution
- **Automatic Cleanup**: Harness CI automatically cleans up `/harness/outputs/` and `/harness/secrets/` after pipeline completion
- **File Permissions**: Secrets written to `/harness/secrets/` have `600` permissions (owner read/write only)
- **No Persistence**: Secrets are not accessible outside the pipeline execution context
- **Pipeline-Scoped**: All secrets scoped to current pipeline execution
- **Automatic Cleanup**: Harness CI removes secrets after pipeline completion
- **File Permissions**: Files have `600` permissions (owner read/write only)
### Best Practices
1. **Use one-time access tokens** instead of permanent credentials when possible
2. **Base64 encode tokens** before storing in Harness secrets
3. **Clean up secret files** after use in pipeline steps
4. **Use appropriate secret scope** (project/org/account)
5. **Verify Record UIDs** before configuring pipelines
1. Use one-time access tokens instead of permanent credentials
2. Base64 encode tokens before storing in Harness secrets
3. Clean up secret files after use in pipeline steps
4. Use appropriate secret scope (project/org/account)
5. Verify Record UIDs before configuring pipelines
## Troubleshooting
### Token Not Found
**Error:** `KSM config is required`
**Solutions:**
- Verify the secret exists in Harness
- Check the secret scope matches your project
- Ensure the expression `<+secrets.getValue("KSM_CONFIG")>` is correct
- Verify secret name matches exactly
### Invalid Token Format
**Error:** `Invalid token format - expected US:xxxxx or JSON config`
**Solutions:**
- Verify the token starts with `US:` (for one-time tokens)
- Check if the token is base64 encoded (it will be auto-decoded)
- For JSON config, ensure it starts with `{` and has required fields: `hostname`, `clientId`, `privateKey`
### Token Already Used / Expired
**Error:** `Failed: token ... invalid/expired`
**Solutions:**
1. Generate a new token in Keeper Security
2. Base64 encode: `echo -n "US:new-token" | base64`
3. Update the Harness secret with the new token
4. Run the pipeline again
**Note:** One-time access tokens are single-use - generate a new token for each use or use JSON config for reusable credentials.
### Secret File Not Found
**Error:** `ERROR: Secret files not found` or `Value not found for notation: <record-uid>/field/<field-type>`
**Solutions:**
- **Verify Record UID is correct**: The Record UID must match exactly (case-sensitive)
- **Check Record UID exists**: Ensure the record exists in Keeper and is shared to your Secrets Manager Application
- **Verify access permissions**: The one-time token or JSON config must have access to the record
- **Check field name**: Ensure the field type (`password`, `login`, `text`, etc.) or custom field label exists in the record
- **Verify Application sharing**: The record must be shared to the Secrets Manager Application you're using
- **Check logs**: `ls -la /harness/secrets/` to see what files were created
- **Verify notation format**: Ensure the notation follows the correct format (e.g., `/field/password`, `/custom_field/MyLabel`, `/file/filename`)
### Expression Not Resolved
**Error:** Token preview shows `${...}` or `<+...>` unresolved
**Solutions:**
- Use `envVariables` (not `settings`) for Harness expressions when passing `KSM_CONFIG` to the plugin
- Verify secret name matches exactly
- Check secret scope matches the reference
- Ensure you're using the correct Harness expression syntax: `<+secrets.getValue("SECRET_NAME")>`
### Harness Expression Not Resolved
**Error:** `Harness expression not resolved`
**Solutions:**
- Ensure `KSM_CONFIG` is passed via `envVariables`, not `settings`
- Verify the secret exists and is accessible in the current scope
- Check that the Harness expression syntax is correct: `<+secrets.getValue("SECRET_NAME")>`
## Local Development
### Test Locally
```bash
# Set environment variables
export PLUGIN_SECRETS="JQPso5SMBpP29gKKGp6Enw/field/password > env:DB_PASSWORD"
export KSM_CONFIG="VVM6..." # Your base64 encoded token or JSON config
# Run the plugin
node ./src/index.js
```
### Project Structure
```
harness_keeper_plugin/
├── src/
│ └── index.js # Main plugin logic
├── entrypoint.sh # Container entrypoint script
├── Dockerfile # Docker image definition
├── package.json # Node.js dependencies
├── Example/
│ └── Pipeline.yaml # Example Harness CI pipeline
└── README.md # This file
```
## Example Pipeline
See `Example/Pipeline.yaml` for a complete working example that demonstrates:
- Plugin configuration
- Secret mapping with multiple record UIDs
- File-based secret consumption
- Error handling and failure strategies
## Support
For issues or questions:
- Check the [Keeper Security Documentation](https://docs.keeper.io/secrets-manager/)
- Review [Keeper Notation Documentation](https://docs.keeper.io/secrets-manager/secrets-manager/keeper-notation/)
- Review Harness CI documentation for plugin configuration
- Verify token format and Keeper record permissions
## License
ISC
| Error | Solution |
|-------|----------|
| `KSM config is required` | Verify secret exists and expression `<+secrets.getValue("SECRET_NAME")>` is correct |
| `Invalid token format` | Check token starts with `US:` or `{` for JSON config |
| `Value not found for notation` | Verify Record UID, field name (case-sensitive), and Application permissions |
| `Failed to download file` | Check file exists in record, name matches exactly, and Application has file access permissions |
| `Harness expression not resolved` | Verify secret reference expression and secret scope (project/org/account) |
## Version History
@@ -478,3 +228,8 @@ ISC
- File-based secret access
- Base64 token decoding
- Secure file permissions and cleanup
## References
- [Keeper Notation Documentation](https://docs.keeper.io/en/keeperpam/secrets-manager/about/keeper-notation)
- [Keeper Secrets Manager Quick Start Guide](https://docs.keeper.io/en/keeperpam/secrets-manager/quick-start-guide)
+31 -8
View File
@@ -120,19 +120,42 @@ const runPlugin = async () => {
continue;
}
const notationIsFile = input.notation.includes('/file/');
const isFileReference = typeof secret === 'object' &&
secret !== null &&
(secret.fileId || secret.fileUid || secret.url || notationIsFile);
let data;
if (isFileReference) {
try {
const fileData = await downloadFile(secret);
data = fileData instanceof Uint8Array ? Buffer.from(fileData) :
Buffer.isBuffer(fileData) ? fileData : Buffer.from(fileData);
} catch (downloadError) {
core.error(`Failed to download file for notation ${input.notation}: ${downloadError.message}`);
continue;
}
} else {
data = Buffer.isBuffer(secret) ? secret :
typeof secret === 'string' ? Buffer.from(secret, 'utf8') :
Buffer.from(String(secret), 'utf8');
}
if (input.destinationType === 'file') {
const fullPath = path.resolve(input.destination);
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
const data = (typeof secret === 'object' && secret.fileId)
? await downloadFile(secret)
: secret;
fs.writeFileSync(fullPath, data);
} else if (input.destinationType === 'environment') {
// Output with ENV: prefix for environment variables
console.log(`ENV:${input.destination}='${secret}'`);
} else {
// Output variable (for Harness output variables)
console.log(`OUT:${input.destination}='${secret}'`);
fs.mkdirSync('/harness/secrets', { recursive: true });
const secretFilePath = path.join('/harness/secrets', input.destination);
fs.writeFileSync(secretFilePath, data);
fs.chmodSync(secretFilePath, 0o600);
if (input.destinationType === 'environment') {
const outputValue = Buffer.isBuffer(data) ? data.toString('utf8') : String(data);
console.log(`ENV:${input.destination}='${outputValue}'`);
}
}
}
} catch (error) {