npm package: @http-forge/core
Description: Headless HTTP testing engine with Postman collection support, dynamic variables, and script-based automation.
Author: Henry Huang
Latest version: 0.2.3

# @http-forge/core

**Standalone HTTP testing engine with Postman collection support and JavaScript-based automation.**

[![npm version](https://img.shields.io/npm/v/@http-forge/core.svg)](https://www.npmjs.com/package/@http-forge/core)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## 📦 What is @http-forge/core?

`@http-forge/core` is a headless, framework-agnostic HTTP execution engine with **full Postman collection compatibility**. Execute complex API workflows, test suites, and automated flows without the overhead of a UI.

**Core Features:**
- 🚀 **Postman Collections** - Load and execute `.postman_collection.json` and `.forge.json` files
- 📝 **JavaScript Scripting** - Pre-request and post-response scripts with full `pm.*` API (variables, assertions, execution flow, visualizer)
- 🔄 **Dynamic Variables** - Built-in generators: `{{$randomInt}}`, `{{$timestamp}}`, `{{$uuid}}`, `{{$guid}}`, etc.
- 🌍 **Environments** - Full variable scoping (globals, collection, environment, session, iterationData)
- 🍪 **Cookie Persistence** - Automatic cookie storage and reuse, `pm.cookies.jar()` and `.toObject()`
- 📊 **Test Assertions** - BDD-style testing with `pm.test()` (sync/async) and full Chai `expect()` chains
- 🔐 **CryptoJS** - Full crypto library: hash, HMAC, AES/DES/TripleDES, PBKDF2, encoding helpers
- 🎯 **Execution Flow** - `pm.setNextRequest()`, `pm.execution.skipRequest()` for suite runner flow control
- 📈 **Visualizer** - `pm.visualizer.set(template, data)` for custom Handlebars-based HTML output
- 🔌 **Extensible** - Custom interceptors, HTTP clients, and module loaders

**Ideal for:**
- CI/CD pipeline integration (GitHub Actions, GitLab CI, Jenkins)
- Headless API testing and contract validation
- Building custom API testing CLIs
- Load testing and performance monitoring
- Automated integration test suites

## 🎯 Installation

```bash
npm install @http-forge/core
```

Or using the tarball:
```bash
npm install ./http-forge-core-0.1.0.tgz
```

## ⚡ Quick Start

### Basic Usage

```typescript
import { ForgeContainer } from '@http-forge/core';

// Create a container with default settings
const forge = new ForgeContainer();

// Load a collection
const collection = await forge.loadCollection('./my-api.forge.json');

// Execute a request
const result = await forge.execute(collection.items[0], collection);

console.log(result.response.status);        // 200
console.log(result.response.body);          // Response data
console.log(result.postResponseResult?.assertions);  // Test results
```

### With Environment Variables

```typescript
const forge = new ForgeContainer();

// Set environment variables
forge.setEnvironment({
    baseUrl: 'https://api.example.com',
    apiKey: 'your-api-key',
    timeout: '5000'
});

// Variables are automatically interpolated in requests
// URL: {{baseUrl}}/users -> https://api.example.com/users
const result = await forge.execute(request, collection);
```

### With Custom Configuration

```typescript
const forge = new ForgeContainer({
    // Use native Node.js http/https instead of fetch
    useNativeHttp: true,
    
    // Enable automatic cookie management
    enableCookies: true,
    
    // Set request timeout
    requestTimeout: 10000,
    
    // Enable request history
    enableHistory: true,
    maxHistoryEntries: 50,
    
    // Storage format
    storageFormat: 'folder'  // or 'file'
});
```

## 📚 Core Concepts

### ForgeContainer

The main entry point - a dependency injection container that wires up all components.

```typescript
const forge = new ForgeContainer(options);

// Load collections
const collection = await forge.loadCollection(path);
const folderCollection = await forge.loadFolderCollection(path);

// Execute requests
const result = await forge.execute(request, collection, options);

// Manage environments
forge.setEnvironment(variables);
forge.setActiveEnvironment(name);
const resolved = forge.getResolvedEnvironment();

// Access services
const executor = forge.getRequestExecutor();
const loader = forge.getCollectionLoader();
```

### Request Execution

Execute requests with full control over the execution pipeline:

```typescript
const result = await forge.execute(request, collection, {
    environment: 'production',
    overrides: {
        url: 'https://override.com/api',
        headers: { 'X-Custom': 'value' }
    },
    skipPreRequest: false,
    skipPostResponse: false,
    timeout: 5000
});

// Access results
console.log(result.response);           // HTTP response
console.log(result.preRequestResult);   // Pre-request script output
console.log(result.postResponseResult); // Test results
```

### Dynamic Variables

Automatic variable generation within request templates:

```javascript
// URL with dynamic timestamp
https://api.example.com/events?timestamp={{$timestamp}}

// Headers with unique ID
X-Request-ID: {{$uuid}}

// Query parameters with random value
?page=1&seed={{$randomInt:1:100}}
```

**Supported dynamic variables:**
- `{{$randomInt}}` - Random integer (0-2147483647)
- `{{$randomInt:min:max}}` - Random integer in range
- `{{$timestamp}}` - Current Unix timestamp (seconds)
- `{{$uuid}}` - UUID v4
- `{{$guid}}` - GUID (alias for uuid)
- `{{$randomString}}` - 10-char alphanumeric string
- `{{$randomHexadecimal}}` - Random hex string
- `{{$isoTimestamp}}` - ISO 8601 timestamp

### Script Execution

Run pre-request and post-response scripts with full Postman API compatibility:

**Pre-request script - Set variables & modify request:**
```javascript
// Set variables across scopes
pm.variables.set('requestId', pm.variables.randomUUID());
pm.environment.set('token', 'abc-123');
pm.collectionVariables.set('counter', '1');

// Modify request headers
pm.request.headers.add({
    name: 'X-Request-ID',
    value: pm.variables.get('requestId')
});
pm.request.headers.update({
    name: 'Authorization',
    value: 'Bearer ' + pm.environment.get('token')
});

// Modify URL and body
pm.request.url = 'https://api.example.com' + pm.request.url;
pm.request.body.raw = JSON.stringify({ timestamp: Date.now() });

// Set cookies for next request
pm.cookies.set('sessionId', 'sess_abc123');
```

**Post-response script - Test & extract data:**
```javascript
// Run assertions
pm.test('Status is 200', () => {
    pm.expect(pm.response.code).to.equal(200);
});
pm.test('Response time under 1s', () => {
    pm.expect(pm.response.responseTime).to.be.below(1000);
});

// Extract data for next request
const data = pm.response.json();
pm.environment.set('userId', data.id);
pm.environment.set('authToken', data.token);

// Store cookies from response
if (pm.response.headers.has('Set-Cookie')) {
    pm.cookies.set('authCookie', data.authCookie);
}
```

### Environment Management

```typescript
// Define multiple environments
forge.setEnvironmentConfig({
    dev: { baseUrl: 'https://dev.api.com', apiKey: 'dev-key' },
    staging: { baseUrl: 'https://staging.api.com', apiKey: 'staging-key' },
    prod: { baseUrl: 'https://api.com', apiKey: 'prod-key' }
});

// Switch environments
forge.setActiveEnvironment('prod');

// Get resolved variables (with inheritance and overrides)
const vars = forge.getResolvedEnvironment('prod');
```

## 🔧 Advanced Features

### Custom HTTP Client

Implement your own HTTP client:

```typescript
import { IHttpClient, HttpRequest, HttpResponse } from '@http-forge/core';

class CustomHttpClient implements IHttpClient {
    async send(request: HttpRequest): Promise<HttpResponse> {
        // Your custom HTTP logic
        return {
            status: 200,
            statusText: 'OK',
            headers: {},
            body: {},
            duration: 100,
            size: 1024
        };
    }
}

const forge = new ForgeContainer({
    httpClient: new CustomHttpClient()
});
```

### Request/Response Interceptors

Add custom interceptors to modify requests and responses:

```typescript
import { IRequestInterceptor, IResponseInterceptor } from '@http-forge/core';

// Request interceptor
class AuthInterceptor implements IRequestInterceptor {
    async intercept(request: HttpRequest): Promise<HttpRequest> {
        request.headers['Authorization'] = `Bearer ${getToken()}`;
        return request;
    }
}

// Response interceptor
class LoggingInterceptor implements IResponseInterceptor {
    async intercept(response: HttpResponse, request: HttpRequest): Promise<HttpResponse> {
        console.log(`${request.method} ${request.url} -> ${response.status}`);
        return response;
    }
}

const forge = new ForgeContainer({
    requestInterceptors: [new AuthInterceptor()],
    responseInterceptors: [new LoggingInterceptor()]
});
```

### Cookie Management & Persistence

Automatic cookie storage and reuse across multi-request flows:

```typescript
const forge = new ForgeContainer({
    enableCookies: true  // Cookies persist across requests in session
});

// Login - response sets Session-ID cookie
const loginResult = await forge.execute(loginRequest, collection);

// Subsequent requests automatically include Session-ID
// No need to manually extract and re-add cookies
const dataResult = await forge.execute(dataRequest, collection);
const updateResult = await forge.execute(updateRequest, collection);
```

**Access cookies in scripts:**
```javascript
// Pre-request script - read stored cookies
if (pm.cookies.has('sessionId')) {
    const sid = pm.cookies.get('sessionId');
    pm.request.headers.add({
        name: 'Cookie',
        value: 'sessionId=' + sid
    });
}

// Post-response script - store new cookies
pm.response.cookies.forEach(cookie => {
    pm.cookies.set(cookie.name, cookie.value);
});

// List all active cookies
const allCookies = pm.cookies.list();  // [{name, value}, ...]

// Clear cookies
pm.cookies.clear();  // When switching users/sessions
```

Cookies are automatically extracted from `Set-Cookie` response headers and reused in subsequent `Cookie` request headers.

### Request History

Track all executed requests:

```typescript
const forge = new ForgeContainer({
    enableHistory: true,
    maxHistoryEntries: 100
});

// Execute requests
await forge.execute(request1, collection);
await forge.execute(request2, collection);

// Access history
const history = forge.getRequestHistory();
const entries = history.getAll();         // All requests
const byId = history.getByRequestId(id);  // Specific request history
```

## 📖 API Reference

### ForgeContainer

**Constructor Options:**
```typescript
interface ForgeContainerOptions {
    forgeRoot?: string;              // Path to http-forge folder
    storageFormat?: 'file' | 'folder'; // Collection storage format
    
    // HTTP Settings
    useNativeHttp?: boolean;         // Use native http/https
    httpClient?: IHttpClient;        // Custom HTTP client
    httpSettings?: RequestSettings;  // Default HTTP settings
    requestTimeout?: number;         // Request timeout (ms)
    
    // Cookie Settings
    enableCookies?: boolean;         // Enable cookie jar
    cookieJar?: ICookieJar;         // Custom cookie jar
    
    // Interceptors
    requestInterceptors?: IRequestInterceptor[];
    responseInterceptors?: IResponseInterceptor[];
    errorInterceptors?: IErrorInterceptor[];
    
    // Script Settings
    scriptRunner?: IScriptRunner;    // Custom script runner
    scriptTimeout?: number;          // Script timeout (ms)
    
    // History
    enableHistory?: boolean;         // Enable request history
    maxHistoryEntries?: number;      // Max history size
}
```

**Methods:**
```typescript
// Collection loading
loadCollection(path: string): Promise<UnifiedCollection>
loadFolderCollection(path: string): Promise<UnifiedCollection>

// Request execution
execute(
    request: UnifiedRequest,
    collection: UnifiedCollection,
    options?: ExecuteOptions
): Promise<ExecuteResult>

// Environment management
setEnvironment(variables: Record<string, string>): void
setEnvironmentConfig(config: EnvironmentConfig): void
setActiveEnvironment(name: string): void
getResolvedEnvironment(name?: string): Record<string, string>

// Service access
getRequestExecutor(): RequestExecutor
getCollectionLoader(): ICollectionLoader
getEnvironmentResolver(): EnvironmentResolver
getRequestHistory(): IRequestHistory
```

### ExecuteResult

```typescript
interface ExecuteResult {
    response: HttpResponse;          // HTTP response
    preRequestResult?: ScriptResult; // Pre-request script output
    postResponseResult?: ScriptResult; // Test results
    requestId: string;               // Unique request ID
    timestamp: number;               // Execution timestamp
}
```

### HttpResponse

```typescript
interface HttpResponse {
    status: number;                  // HTTP status code
    statusText: string;              // Status text
    headers: Record<string, string>; // Response headers
    body: any;                       // Parsed response body
    cookies?: Cookie[];              // Response cookies
    duration: number;                // Request duration (ms)
    size: number;                    // Response size (bytes)
    redirected?: boolean;            // Whether redirected
}
```

## 🛠️ Use Cases

### CLI Tool

```typescript
#!/usr/bin/env node
import { ForgeContainer } from '@http-forge/core';

async function runTests(collectionPath: string) {
    const forge = new ForgeContainer();
    const collection = await forge.loadCollection(collectionPath);
    
    for (const request of collection.items) {
        const result = await forge.execute(request, collection);
        const tests = result.postResponseResult?.assertions || [];
        
        console.log(`\n${request.name}: ${result.response.status}`);
        tests.forEach(test => {
            console.log(`  ${test.passed ? '✓' : '✗'} ${test.name}`);
        });
    }
}

runTests(process.argv[2]);
```

### CI/CD Integration

```typescript
import { ForgeContainer } from '@http-forge/core';

async function ciTest() {
    const forge = new ForgeContainer({
        enableCookies: true,
        requestTimeout: 30000
    });
    
    forge.setEnvironment({
        baseUrl: process.env.API_URL,
        apiKey: process.env.API_KEY
    });
    
    const collection = await forge.loadCollection('./api-tests.forge.json');
    
    let failedTests = 0;
    for (const request of collection.items) {
        const result = await forge.execute(request, collection);
        const failed = result.postResponseResult?.assertions?.filter(t => !t.passed) || [];
        failedTests += failed.length;
    }
    
    process.exit(failedTests > 0 ? 1 : 0);
}
```

### Custom Testing Framework

```typescript
import { ForgeContainer } from '@http-forge/core';

class ApiTestRunner {
    private forge: ForgeContainer;
    
    constructor() {
        this.forge = new ForgeContainer({
            enableCookies: true,
            enableHistory: true
        });
    }
    
    async runSuite(suites: TestSuite[]) {
        for (const suite of suites) {
            await this.runTests(suite);
        }
    }
    
    async runTests(suite: TestSuite) {
        const collection = await this.forge.loadCollection(suite.collection);
        
        for (const request of collection.items) {
            const result = await this.forge.execute(request, collection);
            suite.results.push(result);
        }
    }
}
```

### Multi-Request Workflows

Execute dependent request chains with automatic cookie and variable management:

```typescript
import { ForgeContainer } from '@http-forge/core';

async function apiAuthWorkflow() {
    const forge = new ForgeContainer({
        enableCookies: true,  // Cookies persist across requests
        enableHistory: true
    });

    // Request 1: Login (sets session cookie)
    const loginReq = {
        name: 'Login',
        method: 'POST',
        url: 'https://api.example.com/auth/login',
        body: { type: 'raw', content: JSON.stringify({ 
            username: '{{email}}',
            password: '{{password}}'
        })},
        scripts: {
            postResponse: `
                const { token, userId } = pm.response.json();
                pm.environment.set('authToken', token);
                pm.environment.set('userId', userId);
            `
        }
    };
    
    const loginResult = await forge.execute(loginReq, collection);
    console.log('✓ Logged in, token:', forge.getResolvedEnvironment()['authToken']);
    
    // Request 2: Fetch user profile (uses session cookie automatically)
    const profileReq = {
        name: 'Get Profile',
        method: 'GET',
        url: 'https://api.example.com/users/{{userId}}',
        headers: {
            'Authorization': 'Bearer {{authToken}}'  // Uses token from login
        }
    };
    
    const profileResult = await forge.execute(profileReq, collection);
    console.log('✓ Profile:', profileResult.response.body);
    
    // Request 3: Update profile (session cookie still active)
    const updateReq = {
        name: 'Update Profile',
        method: 'PATCH',
        url: 'https://api.example.com/users/{{userId}}',
        headers: {
            'Authorization': 'Bearer {{authToken}}'
        },
        body: { type: 'raw', content: JSON.stringify({ status: 'active' })}
    };
    
    const updateResult = await forge.execute(updateReq, collection);
    console.log('✓ Updated profile');
    
    // Session automatically logged out - cookies cleared
    forge.getRequestHistory().clear();
}
```

## 📦 Storage Formats

### File Format (Single JSON)
```
my-api.forge.json
```

### Folder Format (Directory Structure)
```
my-api/
  collection.json
  requests/
    login/
      request.json
    users/
      get-users/
        request.json
      create-user/
        request.json
```

## 🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## 📄 License

MIT © Henry Huang

## 🔗 Links

- [HTTP Forge Extension](https://marketplace.visualstudio.com/items?itemName=your-publisher.http-forge)
- [GitHub Repository](https://github.com/hsl1230/http-forge)
- [Issues](https://github.com/hsl1230/http-forge/issues)

## 📝 Changelog

### 0.2.0 (Postman API Parity)

- ✅ **Async `pm.test()` support** - Tests with async callbacks are properly awaited
- ✅ **Expect chain assertions** - `.a(type)`, `.an(type)`, `.deep.equal()`, `.lengthOf()`, `.exist`, `.members()`, `.keys()`, `.string()`
- ✅ **`pm.iterationData`** scope - Data-driven testing with iteration variables
- ✅ **Response status getters** - `response.to.be.ok`, `.error`, `.clientError`, `.serverError` work as getters and functions
- ✅ **Response headers HeaderList** - `.get()`, `.has()`, `.toObject()`, `.each()` on `pm.response.headers`
- ✅ **`pm.cookies.toObject()`** - Flat `{name: value}` cookie map
- ✅ **`replaceIn()` on all scopes** - Available on `pm.variables`, `pm.environment`, `pm.collectionVariables`, `pm.globals`
- ✅ **Sandbox globals** - `xml2Json()`, `jsonStringify()`, `jsonParse()` available in scripts
- ✅ **Request headers API** - `.toObject()`, `.each()` on `pm.request.headers`
- ✅ **`pm.execution.setNextRequest()`** - Runner flow control (jump to named request or stop with `null`)
- ✅ **`pm.execution.skipRequest()`** - Skip HTTP call from pre-request scripts
- ✅ **`pm.setNextRequest()`** - Top-level alias for flow control
- ✅ **`pm.visualizer.set(template, data)`** - Custom HTML visualization with Handlebars templates
- ✅ **`pm.request.url` as Url object** - Postman SDK-compatible Url with `getHost()`, `getPath()`, `getQueryString()`, `protocol`, `host`, `port`, `path`, `query`, `hash`
- ✅ **Full CryptoJS** - AES/DES/TripleDES encrypt/decrypt, PBKDF2, all hash algorithms (SHA1/256/384/512/SHA3/MD5/RIPEMD160), HMAC, encoding helpers (Hex/Base64/Utf8/Latin1/Utf16)

### 0.1.0 (Initial Release)

- ✅ **Core request execution** with Postman collection support
- ✅ **Dynamic variables** - 7 generators for on-the-fly value generation
- ✅ **Postman-compatible scripting** - `pm.*` API with full feature parity
- ✅ **Variable scoping** - globals, collection, environment, session, flow-level
- ✅ **Cookie persistence** - automatic storage and reuse across request chains
- ✅ **Pre-request & post-response scripts** with shared VM context
- ✅ **Test assertions** with BDD-style `pm.test()` and expect chains
- ✅ **Environment management** with variable substitution
- ✅ **Cookie jar** with domain-aware matching
- ✅ **Request/response interceptors** for customization
- ✅ **Request history** tracking
- ✅ **File and folder collection formats** (Postman-compatible)
- ✅ **Full TypeScript support** with comprehensive types
- ✅ **Dependency injection** for extensibility
- ✅ **Comprehensive test coverage**