# TRO-K10: Build CI/CD obfuscation pipeline step
**Area:** Widget.js Telnyx Obfuscation
**Model:** moonshotai/kimi-k2.5
**Tokens:** 8,362
**Cost:** $0.0089
**Time:** 4363ms
**Timestamp:** 2026-02-16T12:41:36.326873

---

 Here is the complete, production-ready CI/CD obfuscation pipeline for the Sunaiva Talking Widget. The solution includes a type-hinted Python orchestrator (for the Python-centric backend team), a POSIX-compliant shell wrapper, a comprehensive GitHub Actions workflow, and a full pytest suite.

### 1. Python Obfuscation Orchestrator

**File:** `scripts/obfuscate_widget.py`

```python
#!/usr/bin/env python3
"""
Sunaiva Talking Widget - Obfuscation Pipeline

Production-grade JavaScript obfuscation with cryptographic integrity verification,
source map generation, and Gzip pre-compression for CDN deployment.

Tech Stack Integration:
- Python 3.11+ (type hints, dataclasses, pathlib)
- javascript-obfuscator (Node.js)
- SHA-256/512 checksums
- Structured logging
"""

import argparse
import gzip
import hashlib
import json
import logging
import os
import subprocess
import sys
import tempfile
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from enum import Enum
from pathlib import Path
from typing import Dict, Optional, Tuple, Union

# -----------------------------------------------------------------------------
# Constants & Configuration
# -----------------------------------------------------------------------------
class HashAlgorithm(str, Enum):
    SHA256 = "sha256"
    SHA512 = "sha512"

DEFAULT_OBFUSCATOR_BIN = "javascript-obfuscator"
SUPPORTED_EXTENSIONS = {".js", ".mjs"}
MAX_COMPRESSION_LEVEL = 9

# -----------------------------------------------------------------------------
# Data Models
# -----------------------------------------------------------------------------
@dataclass(frozen=True)
class ChecksumEntry:
    """Immutable checksum record for a single artifact."""
    algorithm: str
    hash: str
    size_bytes: int
    compressed_hash: Optional[str] = None
    compressed_size_bytes: Optional[int] = None

@dataclass
class ObfuscationManifest:
    """Manifest documenting the obfuscation run and artifact integrity."""
    version: str = "1.0.0"
    timestamp: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
    git_commit: Optional[str] = None
    git_branch: Optional[str] = None
    input_file: str = ""
    obfuscator_config: Optional[str] = None
    artifacts: Dict[str, ChecksumEntry] = field(default_factory=dict)
    
    def to_json(self, indent: int = 2) -> str:
        return json.dumps(asdict(self), indent=indent, sort_keys=True)
    
    @classmethod
    def from_json(cls, data: str) -> "ObfuscationManifest":
        parsed = json.loads(data)
        # Convert nested dicts back to ChecksumEntry objects
        if "artifacts" in parsed:
            parsed["artifacts"] = {
                k: ChecksumEntry(**v) for k, v in parsed["artifacts"].items()
            }
        return cls(**parsed)

# -----------------------------------------------------------------------------
# Custom Exceptions
# -----------------------------------------------------------------------------
class PipelineError(Exception):
    """Base exception for obfuscation pipeline failures."""
    pass

class EnvironmentError(PipelineError):
    """Missing dependencies or invalid environment."""
    pass

class ObfuscationError(PipelineError):
    """javascript-obfuscator execution failure."""
    pass

class ChecksumError(PipelineError):
    """Checksum generation or verification failure."""
    pass

# -----------------------------------------------------------------------------
# Core Orchestrator
# -----------------------------------------------------------------------------
class WidgetObfuscator:
    """
    Production-ready widget obfuscation orchestrator.
    
    Handles the full lifecycle: validation → obfuscation → source maps → 
    checksums → compression → manifest generation.
    """
    
    def __init__(
        self,
        input_path: Path,
        output_dir: Path,
        config_path: Optional[Path] = None,
        hash_algorithm: HashAlgorithm = HashAlgorithm.SHA256,
        compress: bool = True,
        log_level: int = logging.INFO
    ):
        self.input_path = Path(input_path).resolve()
        self.output_dir = Path(output_dir).resolve()
        self.config_path = Path(config_path).resolve() if config_path else None
        self.hash_algorithm = hash_algorithm
        self.compress = compress
        self.logger = self._setup_logging(log_level)
        
        # Derived paths
        self.minified_path = self.output_dir / "widget.min.js"
        self.sourcemap_path = self.output_dir / "widget.min.js.map"
        self.manifest_path = self.output_dir / "manifest.json"
        self.compressed_path = self.output_dir / "widget.min.js.gz"
        
    def _setup_logging(self, level: int) -> logging.Logger:
        """Configure structured logging for CI environments."""
        logger = logging.getLogger("WidgetObfuscator")
        logger.setLevel(level)
        
        if not logger.handlers:
            handler = logging.StreamHandler(sys.stdout)
            formatter = logging.Formatter(
                '%(asctime)s | %(levelname)-8s | %(name)s | %(message)s',
                datefmt='%Y-%m-%d %H:%M:%S'
            )
            handler.setFormatter(formatter)
            logger.addHandler(handler)
            
        return logger
        
    def validate_environment(self) -> None:
        """
        Verify Node.js, npm, and javascript-obfuscator are available.
        
        Raises:
            EnvironmentError: If any dependency is missing or incompatible.
        """
        self.logger.info("Validating build environment...")
        
        # Check Node.js
        try:
            result = subprocess.run(
                ["node", "--version"],
                capture_output=True,
                text=True,
                check=True,
                timeout=10
            )
            node_version = result.stdout.strip()
            self.logger.debug(f"Found Node.js: {node_version}")
        except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError) as e:
            raise EnvironmentError(f"Node.js is required but not found or not working: {e}")
            
        # Check javascript-obfuscator
        try:
            result = subprocess.run(
                [DEFAULT_OBFUSCATOR_BIN, "--version"],
                capture_output=True,
                text=True,
                check=True,
                timeout=10
            )
            self.logger.debug(f"Found {DEFAULT_OBFUSCATOR_BIN}: {result.stdout.strip()}")
        except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError) as e:
            raise EnvironmentError(
                f"{DEFAULT_OBFUSCATOR_BIN} is not installed. "
                f"Run: npm install -g javascript-obfuscator"
            ) from e
            
        # Validate input file
        if not self.input_path.exists():
            raise EnvironmentError(f"Input file not found: {self.input_path}")
            
        if self.input_path.suffix not in SUPPORTED_EXTENSIONS:
            raise EnvironmentError(f"Unsupported file type: {self.input_path.suffix}")
            
        # Validate config if provided
        if self.config_path and not self.config_path.exists():
            raise EnvironmentError(f"Obfuscator config not found: {self.config_path}")
            
        # Ensure output directory exists
        self.output_dir.mkdir(parents=True, exist_ok=True)
        self.logger.info("Environment validation passed")
        
    def _calculate_checksum(self, file_path: Path, algorithm: HashAlgorithm) -> Tuple[str, int]:
        """
        Calculate cryptographic hash and file size.
        
        Returns:
            Tuple of (hex_digest, size_in_bytes)
        """
        hasher = hashlib.new(algorithm.value)
        size = 0
        
        # Stream large files to avoid memory issues
        with open(file_path, "rb") as f:
            while chunk := f.read(8192):
                hasher.update(chunk)
                size += len(chunk)
                
        return hasher.hexdigest(), size
        
    def _compress_gzip(self, source: Path, dest: Path, level: int = MAX_COMPRESSION_LEVEL) -> Tuple[str, int]:
        """
        Gzip compress artifact for CDN deployment.
        
        Returns:
            Tuple of (hash, size) of compressed file
        """
        self.logger.info(f"Compressing {source.name} -> {dest.name}")
        
        with open(source, "rb") as f_in:
            with gzip.open(dest, "wb", compresslevel=level) as f_out:
                shutil.copyfileobj(f_in, f_out)
                
        hash_val, size = self._calculate_checksum(dest, self.hash_algorithm)
        self.logger.debug(f"Compressed size: {size} bytes (hash: {hash_val[:16]}...)")
        return hash_val, size
        
    def obfuscate(self) -> None:
        """
        Execute javascript-obfuscator with source map generation.
        
        Raises:
            ObfuscationError: If the obfuscator returns non-zero exit code.
        """
        self.logger.info(f"Starting obfuscation: {self.input_path.name}")
        
        cmd = [
            DEFAULT_OBFUSCATOR_BIN,
            str(self.input_path),
            "--output",
            str(self.minified_path),
            "--source-map",
            "true",
            "--source-map-mode",
            "separate",
            "--source-map-file-name",
            str(self.sourcemap_path)
        ]
        
        if self.config_path:
            cmd.extend(["--config", str(self.config_path)])
            
        self.logger.debug(f"Executing: {' '.join(cmd)}")
        
        try:
            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                check=True,
                timeout=120  # 2 minutes max for large files
            )
            
            if result.stderr:
                self.logger.warning(f"Obfuscator stderr: {result.stderr}")
                
        except subprocess.CalledProcessError as e:
            raise ObfuscationError(
                f"Obfuscation failed (exit {e.returncode}): {e.stderr}"
            ) from e
        except subprocess.TimeoutExpired as e:
            raise ObfuscationError(f"Obfuscation timed out after {e.timeout} seconds") from e
            
        # Verify outputs were created
        if not self.minified_path.exists():
            raise ObfuscationError("Obfuscated output file was not created")
            
        self.logger.info(f"Obfuscation complete: {self.minified_path.name}")
        
    def generate_manifest(self) -> ObfuscationManifest:
        """
        Generate cryptographic checksums for all artifacts.
        
        Returns:
            ObfuscationManifest with integrity data.
        """
        self.logger.info("Generating integrity manifest...")
        
        manifest = ObfuscationManifest(
            git_commit=os.getenv("GITHUB_SHA"),
            git_branch=os.getenv("GITHUB_REF_NAME"),
            input_file=str(self.input_path),
            obfuscator_config=str(self.config_path) if self.config_path else None
        )
        
        # Hash obfuscated JS
        js_hash, js_size = self._calculate_checksum(self.minified_path, self.hash_algorithm)
        entry = ChecksumEntry(
            algorithm=self.hash_algorithm.value,
            hash=js_hash,
            size_bytes=js_size
        )
        
        # Compress if requested (for CDN)
        if self.compress:
            comp_hash, comp_size = self._compress_gzip(self.minified_path, self.compressed_path)
            entry = ChecksumEntry(
                algorithm=self.hash_algorithm.value,
                hash=js_hash,
                size_bytes=js_size,
                compressed_hash=comp_hash,
                compressed_size_bytes=comp_size
            )
            manifest.artifacts[self.compressed_path.name] = entry
            
        manifest.artifacts[self.minified_path.name] = entry
        
        # Hash source map if it exists
        if self.sourcemap_path.exists():
            map_hash, map_size = self._calculate_checksum(self.sourcemap_path, self.hash_algorithm)
            manifest.artifacts[self.sourcemap_path.name] = ChecksumEntry(
                algorithm=self.hash_algorithm.value,
                hash=map_hash,
                size_bytes=map_size
            )
            
        # Write manifest
        self.manifest_path.write_text(manifest.to_json())
        self.logger.info(f"Manifest written: {self.manifest_path}")
        
        return manifest
        
    def verify(self, manifest: Optional[ObfuscationManifest] = None) -> bool:
        """
        Verify artifacts against manifest checksums.
        
        Args:
            manifest: Manifest to verify against. If None, loads from disk.
            
        Returns:
            True if all checksums match.
            
        Raises:
            ChecksumError: If verification fails.
        """
        if manifest is None:
            if not self.manifest_path.exists():
                raise ChecksumError("No manifest found for verification")
            manifest = ObfuscationManifest.from_json(self.manifest_path.read_text())
            
        self.logger.info("Verifying artifact integrity...")
        
        for filename, entry in manifest.artifacts.items():
            file_path = self.output_dir / filename
            if not file_path.exists():
                raise ChecksumError(f"Missing artifact for verification: {filename}")
                
            current_hash, _ = self._calculate_checksum(file_path, HashAlgorithm(entry.algorithm))
            
            # Check compressed or uncompressed hash depending on what's in manifest
            expected_hash = entry.compressed_hash if entry.compressed_hash else entry.hash
            
            if current_hash != expected_hash:
                raise ChecksumError(
                    f"Checksum mismatch for {filename}: "
                    f"expected {expected_hash[:16]}..., got {current_hash[:16]}..."
                )
                
        self.logger.info("Integrity verification passed")
        return True
        
    def run(self) -> ObfuscationManifest:
        """
        Execute full pipeline: validate → obfuscate → manifest → verify.
        
        Returns:
            ObfuscationManifest with all integrity data.
        """
        self.validate_environment()
        self.obfuscate()
        manifest = self.generate_manifest()
        self.verify(manifest)
        
        # Summary
        self.logger.info("=" * 60)
        self.logger.info("OBFUSCATION PIPELINE COMPLETE")
        self.logger.info(f"Output directory: {self.output_dir}")
        for name, entry in manifest.artifacts.items():
            size_kb = entry.size_bytes / 1024
            self.logger.info(f"  {name}: {size_kb:.2f} KB ({entry.hash[:16]}...)")
        self.logger.info("=" * 60)
        
        return manifest

# -----------------------------------------------------------------------------
# CLI Interface
# -----------------------------------------------------------------------------
def main() -> int:
    parser = argparse.ArgumentParser(
        description="Sunaiva Widget Obfuscation Pipeline",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  # Basic obfuscation
  %(prog)s --input widget.js --output-dir dist/
  
  # With config and verification only
  %(prog)s --input widget.js --verify --manifest dist/manifest.json
  
  # Production with SHA-512
  %(prog)s --input widget.js --config obfuscator.json --algorithm sha512
        """
    )
    
    parser.add_argument(
        "--input", "-i",
        type=Path,
        required=True,
        help="Path to raw widget.js"
    )
    parser.add_argument(
        "--output-dir", "-o",
        type=Path,
        default=Path("dist"),
        help="Output directory (default: dist/)"
    )
    parser.add_argument(
        "--config", "-c",
        type=Path,
        help="Path to javascript-obfuscator config JSON"
    )
    parser.add_argument(
        "--algorithm",
        choices=["sha256", "sha512"],
        default="sha256",
        help="Hash algorithm for integrity checks"
    )
    parser.add_argument(
        "--no-compress",
        action="store_true",
        help="Skip Gzip compression"
    )
    parser.add_argument(
        "--verify",
        action="store_true",
        help="Run verification only (requires existing manifest)"
    )
    parser.add_argument(
        "--manifest",
        type=Path,
        help="Path to manifest for verification (default: output-dir/manifest.json)"
    )
    parser.add_argument(
        "--verbose", "-v",
        action="store_true",
        help="Enable debug logging"
    )
    
    args = parser.parse_args()
    
    log_level = logging.DEBUG if args.verbose else logging.INFO
    
    try:
        obfuscator = WidgetObfuscator(
            input_path=args.input,
            output_dir=args.output_dir,
            config_path=args.config,
            hash_algorithm=HashAlgorithm(args.algorithm),
            compress=not args.no_compress,
            log_level=log_level
        )
        
        if args.verify:
            manifest_path = args.manifest or obfuscator.manifest_path
            manifest = ObfuscationManifest.from_json(manifest_path.read_text())
            obfuscator.verify(manifest)
            print("✓ Verification successful")
        else:
            obfuscator.run()
            
        return 0
        
    except PipelineError as e:
        logging.error(f"Pipeline failed: {e}")
        return 1
    except Exception as e:
        logging.exception(f"Unexpected error: {e}")
        return 2

if __name__ == "__main__":
    sys.exit(main())
```

### 2. Standalone Shell Script

**File:** `scripts/obfuscate-widget.sh`

```bash
#!/bin/bash
# Sunaiva Talking Widget - CI/CD Obfuscation Wrapper
# Production-ready shell wrapper for the Python orchestrator
# Usage: ./scripts/obfuscate-widget.sh [input] [output-dir] [config]

set -euo pipefail

# -----------------------------------------------------------------------------
# Configuration
# -----------------------------------------------------------------------------
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
readonly PYTHON_SCRIPT="${SCRIPT_DIR}/obfuscate_widget.py"

# Default paths
INPUT_FILE="${1:-${PROJECT_ROOT}/src/widget.js}"
OUTPUT_DIR="${2:-${PROJECT_ROOT}/dist}"
CONFIG_FILE="${3:-${PROJECT_ROOT}/config/obfuscator-config.json}"

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# -----------------------------------------------------------------------------
# Functions
# -----------------------------------------------------------------------------
log_info() {
    echo -e "${GREEN}[INFO]${NC} $1"
}

log_warn() {
    echo -e "${YELLOW}[WARN]${NC} $1"
}

log_error() {
    echo -e "${RED}[ERROR]${NC} $1"
}

cleanup() {
    if [[ $? -ne 0 ]]; then
        log_error "Obfuscation pipeline failed. Check logs above."
    fi
}

trap cleanup EXIT

check_dependency() {
    local cmd=$1
    local install_hint=$2
    
    if ! command -v "$cmd" &> /dev/null; then
        log_error "Required command not found: $cmd"
        log_info "Install hint: $install_hint"
        exit 1
    fi
}

install_obfuscator() {
    log_info "Checking javascript-obfuscator installation..."
    
    if ! command -v javascript-obfuscator &> /dev/null; then
        log_warn "javascript-obfuscator not found globally. Attempting install..."
        
        check_dependency "npm" "Install Node.js from https://nodejs.org"
        
        # Try global install first (CI environments)
        if npm install -g javascript-obfuscator@latest; then
            log_info "Installed javascript-obfuscator globally"
        else
            # Fallback to local install in temp dir
            local temp_dir=$(mktemp -d)
            log_warn "Global install failed, trying local install in ${temp_dir}"
            (cd "$temp_dir" && npm install javascript-obfuscator)
            export PATH="${temp_dir}/node_modules/.bin:$PATH"
            log_info "Added local node_modules to PATH"
        fi
    else
        local version
        version=$(javascript-obfuscator --version)
        log_info "Found javascript-obfuscator: $version"
    fi
}

validate_inputs() {
    if [[ ! -f "$INPUT_FILE" ]]; then
        log_error "Input file not found: $INPUT_FILE"
        exit 1
    fi
    
    # Ensure output directory exists
    mkdir -p "$OUTPUT_DIR"
    
    log_info "Input:  $INPUT_FILE"
    log_info "Output: $OUTPUT_DIR"
    [[ -f "$CONFIG_FILE" ]] && log_info "Config: $CONFIG_FILE"
}

run_python_obfuscator() {
    local python_cmd
    if command -v python3 &> /dev/null; then
        python_cmd="python3"
    elif command -v python &> /dev/null; then
        python_cmd="python"
    else
        log_error "Python 3 is required but not found"
        exit 1
    fi
    
    log_info "Using Python: $("$python_cmd" --version)"
    
    local args=(
        "$python_cmd" "$PYTHON_SCRIPT"
        "--input" "$INPUT_FILE"
        "--output-dir" "$OUTPUT_DIR"
        "--algorithm" "sha256"
        "--verbose"
    )
    
    if [[ -f "$CONFIG_FILE" ]]; then
        args+=("--config" "$CONFIG_FILE")
    fi
    
    log_info "Starting obfuscation pipeline..."
    "${args[@]}"
}

verify_outputs() {
    log_info "Verifying build artifacts..."
    
    local required_files=(
        "${OUTPUT_DIR}/widget.min.js"
        "${OUTPUT_DIR}/manifest.json"
    )
    
    for file in "${required_files[@]}"; do
        if [[ ! -f "$file" ]]; then
            log_error "Missing required output: $file"
            exit 1
        fi
    done
    
    # Display checksums
    log_info "Build artifacts verified:"
    cat "${OUTPUT_DIR}/manifest.json" | grep -E '"hash"|"size"' || true
}

# -----------------------------------------------------------------------------
# Main Execution
# -----------------------------------------------------------------------------
main() {
    log_info "Sunaiva Widget Obfuscation Pipeline"
    log_info "===================================="
    
    check_dependency "node" "Install Node.js LTS (v18+ recommended)"
    install_obfuscator
    validate_inputs
    run_python_obfuscator
    verify_outputs
    
    log_info "===================================="
    log_info "${GREEN}SUCCESS${NC}: Obfuscation complete"
    log_info "Artifacts available in: ${OUTPUT_DIR}"
}

main "$@"
```

### 3. GitHub Actions Workflow

**File:** `.github/workflows/widget-obfuscation.yml`

```yaml
name: Widget Obfuscation Pipeline

on:
  push:
    branches: [main, develop]
    paths:
      - 'src/widget.js'
      - 'src/widget/**'
      - 'config/obfuscator-config.json'
  pull_request:
    branches: [main]
    paths:
      - 'src/widget.js'
      - 'scripts/obfuscate_widget.py'
      - 'scripts/obfuscate-widget.sh'
  workflow_dispatch:  # Manual trigger

env:
  NODE_VERSION: '20.x'
  PYTHON_VERSION: '3.11'
  WIDGET_INPUT: './src/widget.js'
  OUTPUT_DIR: './dist'
  MANIFEST_FILE: './dist/manifest.json'

jobs:
  obfuscate:
    name: Obfuscate & Verify Widget
    runs-on: ubuntu-latest
    permissions:
      contents: read
      actions: write
      id-token: write  # For S3 upload if needed later
    
    steps:
      # 1. Checkout
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0  # For git commit info in manifest

      # 2. Setup Node.js (for javascript-obfuscator)
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
          cache-dependency-path: '**/package-lock.json'

      # 3. Setup Python (for orchestrator)
      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: ${{ env.PYTHON_VERSION }}
          cache: 'pip'
          cache-dependency-path: 'requirements.txt'

      # 4. Install javascript-obfuscator globally
      - name: Install Obfuscator
        run: |
          npm install -g javascript-obfuscator@latest
          javascript-obfuscator --version

      # 5. Run Obfuscation Pipeline
      - name: Execute Obfuscation
        run: |
          chmod +x ./scripts/obfuscate-widget.sh
          ./scripts/obfuscate-widget.sh \
            "${{ env.WIDGET_INPUT }}" \
            "${{ env.OUTPUT_DIR }}" \
            "./config/obfuscator-config.json"
        env:
          GITHUB_SHA: ${{ github.sha }}
          GITHUB_REF_NAME: ${{ github.ref_name }}

      # 6. Verify Checksums (Redundant safety check)
      - name: Verify Integrity
        run: |
          python3 ./scripts/obfuscate_widget.py \
            --verify \
            --manifest "${{ env.MANIFEST_FILE }}" \
            --input "${{ env.WIDGET_INPUT }}" \
            --output-dir "${{ env.OUTPUT_DIR }}"

      # 7. Upload Artifacts (for debugging and releases)
