Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
CHANGELOG
=========

9.0.0
9.0.0 (unreleased)
------------------

* **Breaking** Dropped support for Node.js 18 and 20. The minimum supported version is
Expand All @@ -15,6 +15,29 @@ CHANGELOG
property (for example, the network error behind a `FETCH_ERROR`). The
`WebServiceError` and `ArgumentError` classes and the `WebServiceClientError`
type are now exported from the package.
* **Breaking** The `WebServiceClient` constructor now takes an options object as
its third argument (`{ timeout, host, fetcher }`) instead of positional
`timeout` and `host` arguments. For backward compatibility a number may still
be passed and is treated as the `timeout`, so only callers that passed `host`
positionally need to change (to `{ host }`). The new `fetcher` option accepts
a custom `fetch` implementation, useful for routing requests through a proxy
or custom dispatcher, or for testing.
* The `code` property on `WebServiceError` and the `WebServiceClientError`
interface is now typed as `WebServiceErrorCode`
(`ClientErrorCode | (string & {})`) instead of `string`, providing
autocompletion for the client-generated codes while still accepting any code
returned by the web service. The `ClientErrorCode` and `WebServiceErrorCode`
types are exported from the package.
* `ArgumentError` now accepts an optional `cause` and forwards it to `Error`.
* `customInputs` on `Transaction` may now be provided as a plain object mapping
keys to values, in addition to an array of `CustomInput` instances.
* Removed the `validator` dependency. Email and domain validation now use
built-in regex helpers, and the order referrer URI is validated with the
`URL` constructor (restricted to `http`/`https`). Validation of a few
uncommon email edge cases may differ slightly.
* Fixed a bug where serializing a `Transaction` (e.g. when sending a request)
mutated the `Billing`, `Shipping`, `CreditCard`, and `Order` objects passed
to it.
* Added the input `/device/tracking_token`. This is the token generated by
the [Device Tracking Add-on](https://dev.maxmind.com/minfraud/track-devices)
for explicit device linking. You may provide this by providing
Expand Down
39 changes: 26 additions & 13 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Responses use snake_case and are converted to camelCase in model constructors:
// Model exposes: response.riskScore, response.fundsRemaining
```

The `camelizeResponse()` utility in `utils.ts` handles deep conversion for response data.
The `camelcaseKeys()` utility in `utils.ts` handles deep conversion for response data.
The `snakecaseKeys()` utility converts request objects to snake_case.

#### 2. **Model Inheritance Hierarchy**
Expand Down Expand Up @@ -182,18 +182,26 @@ Tests use `.spec.ts` files co-located with source:

### Test Patterns

Tests typically use `nock` to mock HTTP responses:
```typescript
import nock from 'nock';
`WebServiceClient` tests inject a custom `fetcher` (via the constructor's
options object) rather than mocking at the HTTP layer. A small `clientWith()`
helper builds a client whose fetcher returns a canned `Response` and records the
requests made, so the request shape (method, path, body, auth header) and the
parsed response can both be asserted directly:

nock('https://minfraud.maxmind.com')
.post('/minfraud/v2.0/score')
.reply(200, { risk_score: 50, id: '...', /* ... */ });
```typescript
const { client, requests } = clientWith(() =>
jsonResponse(200, { risk_score: 50, id: '...', /* ... */ })
);

const response = await client.score(transaction);

expect(requests[0].url).toBe('https://minfraud.maxmind.com/minfraud/v2.0/score');
expect(response.riskScore).toBe(50);
```

Error and timeout cases return the appropriate `Response` (or a rejecting/
signal-aware handler) from the fetcher. The library no longer depends on `nock`.

When adding new fields to models:
1. Update test fixtures/mocks to include the new field
2. Add assertions to verify the field is properly mapped
Expand Down Expand Up @@ -293,28 +301,33 @@ Always update `CHANGELOG.md` for user-facing changes.

API responses use snake_case but must be exposed as camelCase.

**Solution**: Use `camelizeResponse()` for nested objects:
**Solution**: Use `camelcaseKeys()` for nested objects:
```typescript
this.email = this.maybeGet<records.Email>(response, 'email');

private maybeGet<T>(response: Response, prop: keyof Response): T | undefined {
return response[prop] ? (camelizeResponse(response[prop]) as T) : undefined;
return response[prop] ? (camelcaseKeys(response[prop]) as T) : undefined;
}
```

### Problem: Request Validation Not Working

Request components should validate inputs in constructors.

**Solution**: Import validators from the `validator` package:
**Solution**: Validate inputs in the constructor and throw `ArgumentError` on
failure, using built-ins (the library has no validation dependency):

```typescript
import validator from 'validator';
import { isIP } from 'node:net';

if (!validator.isEmail(props.address)) {
throw new ArgumentError('Invalid email address');
if (props.ipAddress != null && isIP(props.ipAddress) === 0) {
throw new ArgumentError('Invalid IP address');
}
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Email and domain checks use small local regex helpers (`src/request/email.ts`),
and URLs are validated with the `URL` constructor (`src/request/order.ts`).

### Problem: Missing Error Handling

The client can throw various error types.
Expand Down
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,19 @@ takes your MaxMind account ID and license key. For example:
const client = new minFraud.Client("1234", "LICENSEKEY");
```

If you would like to use the Sandbox environment, you can
set the `host` parameter to `sandbox.maxmind.com`:
The constructor also takes an optional third argument: an options object with
`timeout` (milliseconds, default `3000`), `host` (default `minfraud.maxmind.com`),
and `fetcher` (a custom `fetch` implementation, e.g. to route requests through a
proxy or custom dispatcher). If you would like to use the Sandbox environment,
set `host` to `sandbox.maxmind.com`:

```js
const client = new minFraud.Client("1234", "LICENSEKEY", 3000, 'sandbox.maxmind.com');
const client = new minFraud.Client("1234", "LICENSEKEY", { host: 'sandbox.maxmind.com' });
```

For backward compatibility, a number may be passed as the third argument and is
treated as the `timeout`, though this is deprecated.

Then create a new `Transaction` object. This represents the transaction that
you are sending to minFraud. Each transaction property is instantiated by creating
a new instance of each property's class. For example:
Expand Down
2 changes: 2 additions & 0 deletions lychee.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ exclude = [
# Placeholders / example URLs used in tests and docstrings
'^https?://example\.(com|org|net)',
'^https?://(www\.)?foobar\.com',
# `http://foo` is a single-label host used to test referrer-URI validation
'^https?://foo/?$',
'^http://google\.com',
'^http://localhost',
'127\.0\.0\.1',
Expand Down
117 changes: 1 addition & 116 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 1 addition & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,10 @@
"@eslint/js": "^10.0.1",
"@types/jest": "^30.0.0",
"@types/node": "^25.0.3",
"@types/validator": "^13.0.0",
"eslint": "^10.0.2",
"eslint-config-prettier": "^10.0.1",
"globals": "^17.0.0",
"jest": "^30.0.0",
"nock": "^14.0.0-beta.16",
"prettier": "^3.0.0",
"ts-jest": "^29.4.0",
"typedoc": "^0.28.1",
Expand All @@ -65,7 +63,6 @@
},
"dependencies": {
"@maxmind/geoip2-node": "^6.3.0",
"maxmind": "^5.0.0",
"validator": "^13.0.0"
"maxmind": "^5.0.0"
}
}
11 changes: 11 additions & 0 deletions src/errors.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,15 @@ describe('ArgumentError', () => {
expect(err.name).toBe('ArgumentError');
expect(err.message).toBe('bad input');
});

it('preserves the underlying cause when provided', () => {
const cause = new RangeError('bad value');
const err = new ArgumentError('bad input', { cause });

expect(err.cause).toBe(cause);
});

it('leaves cause undefined when not provided', () => {
expect(new ArgumentError('bad input').cause).toBeUndefined();
});
});
Loading