Skip to content

Commit

Permalink
Add option to supply custom User-Agent (#63)
Browse files Browse the repository at this point in the history
  • Loading branch information
wKovacs64 authored Jan 27, 2019
1 parent 1c0a0de commit c9f48ab
Show file tree
Hide file tree
Showing 14 changed files with 223 additions and 50 deletions.
47 changes: 32 additions & 15 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ account (email address or username).</p>
## Functions

<dl>
<dt><a href="#exp_module_breach--breach">breach(breachName)</a> ⇒ <code><a href="#breach--object">Promise.&lt;Breach&gt;</a></code> | <code>Promise.&lt;null&gt;</code> ⏏</dt>
<dt><a href="#exp_module_breach--breach">breach(breachName, [options])</a> ⇒ <code><a href="#breach--object">Promise.&lt;Breach&gt;</a></code> | <code>Promise.&lt;null&gt;</code> ⏏</dt>
<dd><p>Fetches data for a specific breach event.</p>
</dd>
<dt><a href="#exp_module_breachedAccount--breachedAccount">breachedAccount(account, [options])</a> ⇒ <code><a href="#breach--object">Promise.&lt;Array.&lt;Breach&gt;&gt;</a></code> | <code>Promise.&lt;null&gt;</code> ⏏</dt>
Expand All @@ -50,18 +50,18 @@ account (email address or username).</p>
<dt><a href="#exp_module_breaches--breaches">breaches([options])</a> ⇒ <code><a href="#breach--object">Promise.&lt;Array.&lt;Breach&gt;&gt;</a></code> ⏏</dt>
<dd><p>Fetches all breach events in the system.</p>
</dd>
<dt><a href="#exp_module_dataClasses--dataClasses">dataClasses()</a> ⇒ <code>Promise.&lt;Array.&lt;string&gt;&gt;</code> | <code>Promise.&lt;null&gt;</code> ⏏</dt>
<dt><a href="#exp_module_dataClasses--dataClasses">dataClasses([options])</a> ⇒ <code>Promise.&lt;Array.&lt;string&gt;&gt;</code> | <code>Promise.&lt;null&gt;</code> ⏏</dt>
<dd><p>Fetches all data classes in the system.</p>
</dd>
<dt><a href="#exp_module_pasteAccount--pasteAccount">pasteAccount(email)</a> ⇒ <code><a href="#paste--object">Promise.&lt;Array.&lt;Paste&gt;&gt;</a></code> | <code>Promise.&lt;null&gt;</code> ⏏</dt>
<dt><a href="#exp_module_pasteAccount--pasteAccount">pasteAccount(email, [options])</a> ⇒ <code><a href="#paste--object">Promise.&lt;Array.&lt;Paste&gt;&gt;</a></code> | <code>Promise.&lt;null&gt;</code> ⏏</dt>
<dd><p>Fetches paste data for a specific account (email address).</p>
</dd>
<dt><a href="#exp_module_pwnedPassword--pwnedPassword">pwnedPassword(password)</a> ⇒ <code>Promise.&lt;number&gt;</code> ⏏</dt>
<dt><a href="#exp_module_pwnedPassword--pwnedPassword">pwnedPassword(password, [options])</a> ⇒ <code>Promise.&lt;number&gt;</code> ⏏</dt>
<dd><p>Fetches the number of times the the given password has been exposed in a
breach (0 indicating no exposure). The password is given in plain text, but
only the first 5 characters of its SHA-1 hash will be submitted to the API.</p>
</dd>
<dt><a href="#exp_module_pwnedPasswordRange--pwnedPasswordRange">pwnedPasswordRange(prefix)</a> ⇒ <code><a href="#pwnedpasswordsuffix--object">Promise.&lt;Array.&lt;PwnedPasswordSuffix&gt;&gt;</a></code> ⏏</dt>
<dt><a href="#exp_module_pwnedPasswordRange--pwnedPasswordRange">pwnedPasswordRange(prefix, [options])</a> ⇒ <code><a href="#pwnedpasswordsuffix--object">Promise.&lt;Array.&lt;PwnedPasswordSuffix&gt;&gt;</a></code> ⏏</dt>
<dd><p>Fetches the SHA-1 hash suffixes for the given 5-character SHA-1 hash prefix.</p>
<p>When a password hash with the same first 5 characters is found in the Pwned
Passwords repository, the API will respond with an HTTP 200 and include the
Expand Down Expand Up @@ -110,7 +110,7 @@ import { breach } from 'hibp';
```
<a name="exp_module_breach--breach"></a>

### breach(breachName) ⇒ [<code>Promise.&lt;Breach&gt;</code>](#breach--object) \| <code>Promise.&lt;null&gt;</code> ⏏
### breach(breachName, [options]) ⇒ [<code>Promise.&lt;Breach&gt;</code>](#breach--object) \| <code>Promise.&lt;null&gt;</code> ⏏
Fetches data for a specific breach event.

**Kind**: global method of [<code>breach</code>](#module_breach)
Expand All @@ -121,6 +121,8 @@ with an Error
| Param | Type | Description |
| --- | --- | --- |
| breachName | <code>string</code> | the name of a breach in the system |
| [options] | <code>object</code> | a configuration object |
| [options.userAgent] | <code>string</code> | a custom string to send as the User-Agent field in the request headers (default: `hibp <version>`) |

**Example**
```js
Expand Down Expand Up @@ -162,6 +164,7 @@ an Error
| [options.domain] | <code>string</code> | a domain by which to filter the results (default: all domains) |
| [options.includeUnverified] | <code>boolean</code> | include "unverified" breaches in the results (by default, only verified breaches are included) |
| [options.truncate] | <code>boolean</code> | truncate the results to only include the name of each breach (default: false) |
| [options.userAgent] | <code>string</code> | a custom string to send as the User-Agent field in the request headers (default: `hibp <version>`) |

**Example**
```js
Expand All @@ -179,7 +182,7 @@ breachedAccount('foo')
```
**Example**
```js
breachedAccount('bar', { includeUnverified: true })
breachedAccount('bar', { includeUnverified: true, userAgent: 'my-app 1.0' })
.then(data => {
if (data) {
// ...
Expand Down Expand Up @@ -227,6 +230,7 @@ objects (an empty array if no breaches were found), or rejects with an Error
| --- | --- | --- |
| [options] | <code>object</code> | a configuration object |
| [options.domain] | <code>string</code> | a domain by which to filter the results (default: all domains) |
| [options.userAgent] | <code>string</code> | a custom string to send as the User-Agent field in the request headers (default: `hibp <version>`) |

**Example**
```js
Expand Down Expand Up @@ -267,13 +271,19 @@ import { dataClasses } from 'hibp';
```
<a name="exp_module_dataClasses--dataClasses"></a>

### dataClasses() ⇒ <code>Promise.&lt;Array.&lt;string&gt;&gt;</code> \| <code>Promise.&lt;null&gt;</code> ⏏
### dataClasses([options]) ⇒ <code>Promise.&lt;Array.&lt;string&gt;&gt;</code> \| <code>Promise.&lt;null&gt;</code> ⏏
Fetches all data classes in the system.

**Kind**: global method of [<code>dataClasses</code>](#module_dataClasses)
**Returns**: <code>Promise.&lt;Array.&lt;string&gt;&gt;</code> \| <code>Promise.&lt;null&gt;</code> - a Promise which resolves to an
array of strings (or null if no data classes were found), or rejects with an
Error

| Param | Type | Description |
| --- | --- | --- |
| [options] | <code>object</code> | a configuration object |
| [options.userAgent] | <code>string</code> | a custom string to send as the User-Agent field in the request headers (default: `hibp <version>`) |

**Example**
```js
dataClasses()
Expand All @@ -299,7 +309,7 @@ import { pasteAccount } from 'hibp';
```
<a name="exp_module_pasteAccount--pasteAccount"></a>

### pasteAccount(email) ⇒ <code><a href="#paste--object">Promise.&lt;Array.&lt;Paste&gt;&gt;</a></code> \| <code>Promise.&lt;null&gt;</code> ⏏
### pasteAccount(email, [options]) ⇒ <code><a href="#paste--object">Promise.&lt;Array.&lt;Paste&gt;&gt;</a></code> \| <code>Promise.&lt;null&gt;</code> ⏏
Fetches paste data for a specific account (email address).

**Kind**: global method of [<code>pasteAccount</code>](#module_pasteAccount)
Expand All @@ -310,6 +320,8 @@ Error
| Param | Type | Description |
| --- | --- | --- |
| email | <code>string</code> | the email address to query |
| [options] | <code>object</code> | a configuration object |
| [options.userAgent] | <code>string</code> | a custom string to send as the User-Agent field in the request headers (default: `hibp <version>`) |

**Example**
```js
Expand Down Expand Up @@ -337,7 +349,7 @@ import { pwnedPassword } from 'hibp';
```
<a name="exp_module_pwnedPassword--pwnedPassword"></a>

### pwnedPassword(password) ⇒ <code>Promise.&lt;number&gt;</code> ⏏
### pwnedPassword(password, [options]) ⇒ <code>Promise.&lt;number&gt;</code> ⏏
Fetches the number of times the the given password has been exposed in a
breach (0 indicating no exposure). The password is given in plain text, but
only the first 5 characters of its SHA-1 hash will be submitted to the API.
Expand All @@ -350,6 +362,8 @@ the password has been exposed in a breach, or rejects with an Error
| Param | Type | Description |
| --- | --- | --- |
| password | <code>string</code> | a password in plain text |
| [options] | <code>object</code> | a configuration object |
| [options.userAgent] | <code>string</code> | a custom string to send as the User-Agent field in the request headers (default: `hibp <version>`) |

**Example**
```js
Expand Down Expand Up @@ -378,7 +392,7 @@ import { pwnedPasswordRange } from 'hibp';
```
<a name="exp_module_pwnedPasswordRange--pwnedPasswordRange"></a>

### pwnedPasswordRange(prefix) ⇒ <code><a href="#pwnedpasswordsuffix--object">Promise.&lt;Array.&lt;PwnedPasswordSuffix&gt;&gt;</a></code> ⏏
### pwnedPasswordRange(prefix, [options]) ⇒ <code><a href="#pwnedpasswordsuffix--object">Promise.&lt;Array.&lt;PwnedPasswordSuffix&gt;&gt;</a></code> ⏏
Fetches the SHA-1 hash suffixes for the given 5-character SHA-1 hash prefix.

When a password hash with the same first 5 characters is found in the Pwned
Expand All @@ -388,15 +402,17 @@ of how many times it appears in the data set. This function parses the
response and returns a more structured format.

**Kind**: global method of [<code>pwnedPasswordRange</code>](#module_pwnedPasswordRange)
**Returns**: <code><a href="#pwnedpasswordsuffix--object">Promise.&lt;Array.&lt;PwnedPasswordSuffix&gt;&gt;</a></code> - a Promise which resolves to an array
of objects, each containing the `suffix` that when matched with the prefix
composes the complete hash, and a `count` of how many times it appears in the
breached password data set, or rejects with an Error
**Returns**: <code><a href="#pwnedpasswordsuffix--object">Promise.&lt;Array.&lt;PwnedPasswordSuffix&gt;&gt;</a></code> - a Promise which resolves to an
array of objects, each containing the `suffix` that when matched with the
prefix composes the complete hash, and a `count` of how many times it appears
in the breached password data set, or rejects with an Error
**See**: https://haveibeenpwned.com/API/v2#SearchingPwnedPasswordsByRange

| Param | Type | Description |
| --- | --- | --- |
| prefix | <code>string</code> | the first 5 characters of a SHA-1 password hash (case insensitive) |
| [options] | <code>object</code> | a configuration object |
| [options.userAgent] | <code>string</code> | a custom string to send as the User-Agent field in the request headers (default: `hibp <version>`) |

**Example**
```js
Expand Down Expand Up @@ -456,6 +472,7 @@ rejects with an Error
| [breachOptions] | <code>object</code> | a configuration object pertaining to breach queries |
| [breachOptions.domain] | <code>string</code> | a domain by which to filter the results (default: all domains) |
| [breachOptions.truncate] | <code>boolean</code> | truncate the results to only include the name of each breach (default: false) |
| [breachOptions.userAgent] | <code>string</code> | a custom string to send as the User-Agent field in the request headers (default: `hibp <version>`) |

**Example**
```js
Expand Down
9 changes: 8 additions & 1 deletion src/breach.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import { Breach } from './types/remote-api.d';
* Fetches data for a specific breach event.
*
* @param {string} breachName the name of a breach in the system
* @param {object} [options] a configuration object
* @param {string} [options.userAgent] a custom string to send as the User-Agent
* field in the request headers (default: `hibp <version>`)
* @returns {(Promise<Breach>|Promise<null>)} a Promise which resolves to an
* object representing a breach (or null if no breach was found), or rejects
* with an Error
Expand All @@ -42,9 +45,13 @@ import { Breach } from './types/remote-api.d';
* });
* @alias module:breach
*/
const breach = (breachName: string): Promise<Breach | null> =>
const breach = (
breachName: string,
options: { userAgent?: string } = {},
): Promise<Breach | null> =>
fetchFromApi(
`/breach/${encodeURIComponent(breachName)}`,
options,
) as Promise<Breach | null>;

/**
Expand Down
11 changes: 7 additions & 4 deletions src/breachedAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { Breach } from './types/remote-api.d';
* the results (by default, only verified breaches are included)
* @param {boolean} [options.truncate] truncate the results to only include
* the name of each breach (default: false)
* @param {string} [options.userAgent] a custom string to send as the User-Agent
* field in the request headers (default: `hibp <version>`)
* @returns {(Promise<Breach[]> | Promise<null>)} a Promise which resolves to an
* array of breach objects (or null if no breaches were found), or rejects with
* an Error
Expand All @@ -28,7 +30,7 @@ import { Breach } from './types/remote-api.d';
* // ...
* });
* @example
* breachedAccount('bar', { includeUnverified: true })
* breachedAccount('bar', { includeUnverified: true, userAgent: 'my-app 1.0' })
* .then(data => {
* if (data) {
* // ...
Expand Down Expand Up @@ -59,6 +61,7 @@ const breachedAccount = (
domain?: string;
includeUnverified?: boolean;
truncate?: boolean;
userAgent?: string;
} = {},
): Promise<Breach[] | null> => {
const endpoint = `/breachedaccount/${encodeURIComponent(account)}?`;
Expand All @@ -72,9 +75,9 @@ const breachedAccount = (
if (options.truncate) {
params.push('truncateResponse=true');
}
return fetchFromApi(`${endpoint}${params.join('&')}`) as Promise<
Breach[] | null
>;
return fetchFromApi(`${endpoint}${params.join('&')}`, {
userAgent: options.userAgent,
}) as Promise<Breach[] | null>;
};

/**
Expand Down
7 changes: 6 additions & 1 deletion src/breaches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { Breach } from './types/remote-api.d';
* @param {object} [options] a configuration object
* @param {string} [options.domain] a domain by which to filter the results
* (default: all domains)
* @param {string} [options.userAgent] a custom string to send as the User-Agent
* field in the request headers (default: `hibp <version>`)
* @returns {Promise<Breach[]>} a Promise which resolves to an array of breach
* objects (an empty array if no breaches were found), or rejects with an Error
* @example
Expand Down Expand Up @@ -38,14 +40,17 @@ import { Breach } from './types/remote-api.d';
const breaches = (
options: {
domain?: string;
userAgent?: string;
} = {},
): Promise<Breach[]> => {
const endpoint = '/breaches?';
const params = [];
if (options.domain) {
params.push(`domain=${encodeURIComponent(options.domain)}`);
}
return fetchFromApi(`${endpoint}${params.join('&')}`) as Promise<Breach[]>;
return fetchFromApi(`${endpoint}${params.join('&')}`, {
userAgent: options.userAgent,
}) as Promise<Breach[]>;
};

/**
Expand Down
9 changes: 7 additions & 2 deletions src/dataClasses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import fetchFromApi from './internal/haveibeenpwned/fetchFromApi';
/**
* Fetches all data classes in the system.
*
* @param {object} [options] a configuration object
* @param {string} [options.userAgent] a custom string to send as the User-Agent
* field in the request headers (default: `hibp <version>`)
* @returns {(Promise<string[]> | Promise<null>)} a Promise which resolves to an
* array of strings (or null if no data classes were found), or rejects with an
* Error
Expand All @@ -20,8 +23,10 @@ import fetchFromApi from './internal/haveibeenpwned/fetchFromApi';
* });
* @alias module:dataClasses
*/
const dataClasses = (): Promise<string[] | null> =>
fetchFromApi('/dataclasses') as Promise<string[] | null>;
const dataClasses = (
options: { userAgent?: string } = {},
): Promise<string[] | null> =>
fetchFromApi('/dataclasses', options) as Promise<string[] | null>;

/**
* A module for retrieving all data classes in the system.
Expand Down
11 changes: 11 additions & 0 deletions src/internal/haveibeenpwned/fetchFromApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,15 @@ describe('internal (haveibeenpwned): fetchFromApi', () => {
expect(breachedAccount('unknown response')).rejects.toMatchSnapshot();
});
});

describe('userAgent option', () => {
it('is passed on as a request header', () => {
const ua = 'custom UA';
return dataClasses({ userAgent: ua }).then(() => {
expect(mockAxios.get).toHaveBeenCalledWith(expect.any(String), {
headers: { 'User-Agent': ua },
});
});
});
});
});
25 changes: 23 additions & 2 deletions src/internal/haveibeenpwned/fetchFromApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,32 @@ const blockedWithRayId = (rayId: string): string =>
* @internal
* @private
* @param {string} endpoint the API endpoint to query
* @param {object} [options] a configuration object
* @param {string} [options.userAgent] a custom string to send as the User-Agent
* field in the request headers (default: `hibp <version>`)
* @returns {Promise<ApiData>} a Promise which resolves to the data resulting
* from the query (or null for 404 Not Found responses), or rejects with an
* Error
*/
export default (endpoint: string): Promise<ApiData> =>
Promise.resolve(axios.get<ApiData>(endpoint))
export default (
endpoint: string,
/* istanbul ignore next: no need to test default empty object */
options: { userAgent?: string } = {},
): Promise<ApiData> => {
const { userAgent } = options;

const config = Object.assign(
{},
userAgent
? {
headers: {
'User-Agent': userAgent,
},
}
: {},
);

return Promise.resolve(axios.get<ApiData>(endpoint, config))
.then(res => res.data)
.catch(err => {
if (err.response) {
Expand All @@ -60,3 +80,4 @@ export default (endpoint: string): Promise<ApiData> =>
throw err;
}
});
};
18 changes: 17 additions & 1 deletion src/internal/pwnedpasswords/fetchFromApi.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import AxiosError from 'AxiosError';
import pwnedPasswordRange from 'pwnedPasswordRange';
import { BAD_REQUEST } from './responses';
import { BAD_REQUEST, OK } from './responses';
import axios from './axiosInstance';

const mockAxios = axios as jest.Mocked<typeof axios>;
Expand Down Expand Up @@ -32,4 +32,20 @@ describe('internal (pwnedpassword): fetchFromApi', () => {
expect(pwnedPasswordRange('unknown response')).rejects.toMatchSnapshot();
});
});

describe('userAgent option', () => {
it('is passed on as a request header', () => {
mockAxios.get.mockResolvedValueOnce({
headers: {},
status: OK.status,
data: '1234\n5678',
});
const ua = 'custom UA';
return pwnedPasswordRange('stuff', { userAgent: ua }).then(() => {
expect(mockAxios.get).toHaveBeenCalledWith(expect.any(String), {
headers: { 'User-Agent': ua },
});
});
});
});
});
Loading

0 comments on commit c9f48ab

Please sign in to comment.