Skip to content
Open
90 changes: 90 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Contributing to Hyperswitch Prism

First off, thank you for considering contributing to Hyperswitch Prism! It's people like you that make Hyperswitch such a great tool.

## Code of Conduct

By participating in this project, you are expected to uphold our [Code of Conduct](./docs/CODE_OF_CONDUCT.md).

## Getting Started

Hyperswitch Prism is a multi-language SDK project powered by a core Rust implementation and UniFFI bindings.

### Prerequisites

Depending on which part of the project you want to contribute to, you will need:

- **Core**: Rust (latest stable)
- **Node.js SDK**: Node.js 18+, npm
- **Python SDK**: Python 3.8+, pip, uv (recommended)
- **Java SDK**: Java 11+, Gradle

### Local Setup

1. Fork and clone the repository.
2. Initialize submodules (if any).
3. Navigate to the language SDK you want to work on (e.g., `cd sdk/javascript`).
4. Follow the language-specific setup instructions in their respective `README.md` files.

For example, to set up the Node.js SDK:
```bash
cd sdk/javascript
npm install
npm run build
```

## How to Contribute

### 1. Find an Issue
Look for open issues labeled `good first issue` or `help wanted`. If you want to work on something else, please open an issue first to discuss it with the maintainers.

### 2. Create a Branch
Create a branch for your changes:
```bash
git checkout -b feature/your-feature-name
```
Or for bugs:
```bash
git checkout -b fix/your-bug-fix
```

### 3. Make Changes
- Write clear, concise, and documented code.
- Ensure your code follows the existing style of the codebase.
- Add tests for any new features or bug fixes.

### 4. Run Tests
Ensure all existing tests pass and your new tests run successfully.
For the JS SDK, you can run the smoke tests:
```bash
npm run test
```

### 5. Commit Your Changes
We follow the [Conventional Commits](https://www.conventionalcommits.org/) specification for commit messages.
- `feat:` for new features
- `fix:` for bug fixes
- `docs:` for documentation changes
- `test:` for adding or modifying tests
- `refactor:` for code refactoring

Example:
```bash
git commit -m "feat(sdk/javascript): add new connector support"
```

### 6. Open a Pull Request
- Push your branch to your fork.
- Open a Pull Request against the `main` branch of the `juspay/hyperswitch-prism` repository.
- Fill out the PR template completely.
- Reference any related issues (e.g., `Fixes #123`).

## Coding Standards

- **Rust**: Use `rustfmt` and `clippy`.
- **TypeScript**: We use strict mode. Ensure all types are properly defined. Avoid `any` where possible.
- **Python**: Use type hints, `black` for formatting, and `flake8` for linting.

## License

By contributing, you agree that your contributions will be licensed under its Apache 2.0 License.
File renamed without changes.
1 change: 1 addition & 0 deletions sdk/javascript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"scripts": {
"build": "tsc && cp -r src/payments/generated dist/src/payments/ && cp src/payments/_generated_flows.js dist/src/payments/",
"start": "node dist/src/index.js",
"test:unit": "tsx --test tests/**/*.test.ts",
"test": "node test_smoke.js"
},
"dependencies": {
Expand Down
29 changes: 24 additions & 5 deletions sdk/javascript/src/http_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,11 @@ export class NetworkError extends Error {
}

/**
* Resolve proxy URL, honoring bypass rules.
* Resolve proxy URL for a given target URL, honoring bypass rules.
*
* @param url - The target origin URL (e.g., 'https://api.stripe.com')
* @param proxy - The proxy configuration options including httpUrl, httpsUrl, and bypassUrls
* @returns The resolved proxy URL string, or null if a bypass rule matches or no proxy is configured
*/
export function resolveProxyUrl(url: string, proxy?: types.IProxyOptions | null): string | null {
if (!proxy) return null;
Expand All @@ -66,8 +70,11 @@ export function resolveProxyUrl(url: string, proxy?: types.IProxyOptions | null)
}

/**
* Generate a cache key from proxy configuration for HTTP client caching.
* Returns empty string when no proxy is configured.
* Generate a deterministic cache key from proxy configuration for HTTP client connection pooling.
* This ensures that dispatchers are properly reused across requests with identical proxy configs.
*
* @param proxy - The proxy configuration options
* @returns A string cache key, or an empty string when no proxy is configured
*/
export function generateProxyCacheKey(proxy?: types.IProxyOptions | null): string {
if (!proxy) return "";
Expand All @@ -82,8 +89,13 @@ export function generateProxyCacheKey(proxy?: types.IProxyOptions | null): strin
}

/**
* Creates a high-performance dispatcher with specialized fintech timeouts.
* (The instance-level connection pool)
* Creates a high-performance undici dispatcher with specialized fintech timeouts.
* Serves as the instance-level connection pool for HTTP requests.
* Automatically configures TLS and proxy tunneling based on the provided configuration.
*
* @param config - The HTTP configuration including timeouts, custom CA certs, and proxy settings
* @returns An undici Dispatcher (either Agent or ProxyAgent) configured for optimal connection reuse
* @throws {NetworkError} If proxy configuration is invalid or dispatcher creation fails
*/
export function createDispatcher(config: types.IHttpConfig): Dispatcher {
let ca: string | Uint8Array | undefined;
Expand Down Expand Up @@ -130,6 +142,13 @@ export function createDispatcher(config: types.IHttpConfig): Dispatcher {

/**
* Standardized network execution engine for Unified Connector Service.
* Handles request timeouts, execution via undici, and error normalization.
*
* @param request - The normalized HTTP request object (url, method, headers, body)
* @param options - HTTP configuration options (timeouts)
* @param dispatcher - The undici Dispatcher instance to use for connection pooling
* @returns A Promise resolving to the normalized HTTP response
* @throws {NetworkError} On connection timeouts, response timeouts, parsing errors, or network failures
*/
export async function execute(
request: HttpRequest,
Expand Down
21 changes: 12 additions & 9 deletions sdk/javascript/src/payments/errors.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// @ts-ignore
import { types } from "./generated/proto";

/**
* Error classes for FFI-level errors.
*
Expand All @@ -10,24 +13,24 @@
* Wraps IntegrationError proto and provides access to proto fields.
*/
export class IntegrationError extends Error {
constructor(public proto: any) {
super(proto.errorMessage || proto.error_message);
constructor(public proto: types.IIntegrationError) {
super(proto.errorMessage || (proto as any).error_message);
}

get errorCode(): string { return this.proto.errorCode || this.proto.error_code; }
get suggestedAction(): string | undefined { return this.proto.suggestedAction || this.proto.suggested_action; }
get docUrl(): string | undefined { return this.proto.docUrl || this.proto.doc_url; }
get errorCode(): string | undefined { return this.proto.errorCode || (this.proto as any).error_code; }
get suggestedAction(): string | undefined { return this.proto.suggestedAction || (this.proto as any).suggested_action; }
get docUrl(): string | undefined { return this.proto.docUrl || (this.proto as any).doc_url; }
}

/**
* Exception raised when res_transformer fails (response transformation error).
* Wraps ConnectorError proto and provides access to proto fields.
*/
export class ConnectorError extends Error {
constructor(public proto: any) {
super(proto.errorMessage || proto.error_message);
constructor(public proto: types.IConnectorError) {
super(proto.errorMessage || (proto as any).error_message);
}

get errorCode(): string { return this.proto.errorCode || this.proto.error_code; }
get httpStatusCode(): number | undefined { return this.proto.httpStatusCode || this.proto.http_status_code; }
get errorCode(): string | undefined { return this.proto.errorCode || (this.proto as any).error_code; }
get httpStatusCode(): number | undefined { return this.proto.httpStatusCode || (this.proto as any).http_status_code; }
}
90 changes: 90 additions & 0 deletions sdk/javascript/tests/http_client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
NetworkError,
resolveProxyUrl,
generateProxyCacheKey,
NetworkErrorCode
} from '../src/http_client';
// @ts-ignore
import { types } from '../src/payments/generated/proto';

test('NetworkError initialization and properties', () => {
const err = new NetworkError(
'Connection failed',
types.NetworkErrorCode.CONNECT_TIMEOUT_EXCEEDED,
504
);

assert.equal(err.message, 'Connection failed');
assert.equal(err.code, types.NetworkErrorCode.CONNECT_TIMEOUT_EXCEEDED);
assert.equal(err.statusCode, 504);
assert.equal(err.errorCode, 'CONNECT_TIMEOUT_EXCEEDED');
assert.equal(err.name, 'NetworkError');
});

test('NetworkError default values', () => {
const err = new NetworkError('Simple error');

assert.equal(err.message, 'Simple error');
assert.equal(err.code, types.NetworkErrorCode.NETWORK_ERROR_CODE_UNSPECIFIED);
assert.equal(err.statusCode, undefined);
assert.equal(err.errorCode, 'NETWORK_ERROR_CODE_UNSPECIFIED');
});

test('resolveProxyUrl returns null when no proxy provided', () => {
assert.equal(resolveProxyUrl('https://api.stripe.com'), null);
assert.equal(resolveProxyUrl('https://api.stripe.com', null), null);
});

test('resolveProxyUrl returns httpUrl when httpsUrl is missing', () => {
const proxy = { httpUrl: 'http://proxy.local:8080' };
assert.equal(resolveProxyUrl('https://api.stripe.com', proxy), 'http://proxy.local:8080');
});

test('resolveProxyUrl returns httpsUrl when both are provided', () => {
const proxy = {
httpUrl: 'http://proxy.local:8080',
httpsUrl: 'https://proxy.local:8443'
};
assert.equal(resolveProxyUrl('https://api.stripe.com', proxy), 'https://proxy.local:8443');
});

test('resolveProxyUrl honors bypassUrls', () => {
const proxy = {
httpsUrl: 'https://proxy.local:8443',
bypassUrls: ['https://api.stripe.com']
};
assert.equal(resolveProxyUrl('https://api.stripe.com', proxy), null);
assert.equal(resolveProxyUrl('https://api.adyen.com', proxy), 'https://proxy.local:8443');
});

test('generateProxyCacheKey handles empty proxy', () => {
assert.equal(generateProxyCacheKey(), '');
assert.equal(generateProxyCacheKey(null), '');
});

test('generateProxyCacheKey combines URLs predictably', () => {
const proxy = {
httpUrl: 'http://proxy.local:8080',
httpsUrl: 'https://proxy.local:8443'
};
assert.equal(generateProxyCacheKey(proxy), 'http://proxy.local:8080|https://proxy.local:8443|');
});

test('generateProxyCacheKey sorts bypassUrls for stable keys', () => {
const proxy1 = {
httpUrl: 'http://proxy.local:8080',
bypassUrls: ['https://api.b.com', 'https://api.a.com']
};
const proxy2 = {
httpUrl: 'http://proxy.local:8080',
bypassUrls: ['https://api.a.com', 'https://api.b.com']
};

const key1 = generateProxyCacheKey(proxy1);
const key2 = generateProxyCacheKey(proxy2);

assert.equal(key1, key2);
assert.equal(key1, 'http://proxy.local:8080||https://api.a.com,https://api.b.com');
});
Loading