diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..2c18cbd406 --- /dev/null +++ b/CONTRIBUTING.md @@ -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. diff --git a/grace/enhacer.md b/grace/enhancer.md similarity index 100% rename from grace/enhacer.md rename to grace/enhancer.md diff --git a/sdk/javascript/package.json b/sdk/javascript/package.json index 8d446f997c..d3c7a99f7f 100644 --- a/sdk/javascript/package.json +++ b/sdk/javascript/package.json @@ -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": { diff --git a/sdk/javascript/src/http_client.ts b/sdk/javascript/src/http_client.ts index 8ccba80dec..a15dd7ff83 100644 --- a/sdk/javascript/src/http_client.ts +++ b/sdk/javascript/src/http_client.ts @@ -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; @@ -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 ""; @@ -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; @@ -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, diff --git a/sdk/javascript/src/payments/errors.ts b/sdk/javascript/src/payments/errors.ts index 86bf268b39..5015a3ff6b 100644 --- a/sdk/javascript/src/payments/errors.ts +++ b/sdk/javascript/src/payments/errors.ts @@ -1,3 +1,6 @@ +// @ts-ignore +import { types } from "./generated/proto"; + /** * Error classes for FFI-level errors. * @@ -10,13 +13,13 @@ * 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; } } /** @@ -24,10 +27,10 @@ export class IntegrationError extends 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; } } diff --git a/sdk/javascript/tests/http_client.test.ts b/sdk/javascript/tests/http_client.test.ts new file mode 100644 index 0000000000..61ec568057 --- /dev/null +++ b/sdk/javascript/tests/http_client.test.ts @@ -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'); +});