Skip to content

Commit 682cff7

Browse files
Implement stealerLogsByWebsiteDomain module (#542)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent 655b473 commit 682cff7

File tree

8 files changed

+265
-0
lines changed

8 files changed

+265
-0
lines changed

.bundlewatch.config.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@
5050
"path": "dist/esm/stealer-logs-by-email.js",
5151
"maxSize": "1 kB"
5252
},
53+
{
54+
"path": "dist/esm/stealer-logs-by-website-domain.js",
55+
"maxSize": "1.1 kB"
56+
},
5357
{
5458
"path": "dist/esm/subscribed-domains.js",
5559
"maxSize": "1.1 kB"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'hibp': minor
3+
---
4+
5+
Add `stealerLogsByWebsiteDomain` module.

API.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,16 @@ supplied email address.</p>
7979
without it will fail (unless you specify a <code>baseUrl</code> to a proxy that inserts
8080
a valid API key on your behalf).</p>
8181
</dd>
82+
<dt><a href="#stealerLogsByWebsiteDomain">stealerLogsByWebsiteDomain(websiteDomain, [options])</a> ⇒ <code>Promise.&lt;Array.&lt;string&gt;&gt;</code> | <code>Promise.&lt;null&gt;</code></dt>
83+
<dd><p>Fetches all stealer log email addresses for a website domain.</p>
84+
<p>The result is an array of strings representing email addresses found in
85+
stealer logs for the specified website domain (e.g., &quot;example.com&quot;).</p>
86+
<p>🔑 <code>haveibeenpwned.com</code> requires an API key from
87+
<a href="https://haveibeenpwned.com/API/Key">https://haveibeenpwned.com/API/Key</a> for the <code>stealerlogsbywebsitedomain</code>
88+
endpoint. The <code>apiKey</code> option here is not explicitly required, but direct
89+
requests made without it will fail (unless you specify a <code>baseUrl</code> to a proxy
90+
that inserts a valid API key on your behalf).</p>
91+
</dd>
8292
<dt><a href="#subscribedDomains">subscribedDomains([options])</a> ⇒ <code><a href="#subscribeddomain--object">Promise.&lt;Array.&lt;SubscribedDomain&gt;&gt;</a></code></dt>
8393
<dd><p>Fetches all subscribed domains for your HIBP account.</p>
8494
<p>Returns domains that have been successfully added to the Domain Search dashboard
@@ -641,6 +651,62 @@ try {
641651
// ...
642652
}
643653
```
654+
<a name="stealerLogsByWebsiteDomain"></a>
655+
656+
## stealerLogsByWebsiteDomain(websiteDomain, [options]) ⇒ <code>Promise.&lt;Array.&lt;string&gt;&gt;</code> \| <code>Promise.&lt;null&gt;</code>
657+
Fetches all stealer log email addresses for a website domain.
658+
659+
The result is an array of strings representing email addresses found in
660+
stealer logs for the specified website domain (e.g., "example.com").
661+
662+
🔑 `haveibeenpwned.com` requires an API key from
663+
https://haveibeenpwned.com/API/Key for the `stealerlogsbywebsitedomain`
664+
endpoint. The `apiKey` option here is not explicitly required, but direct
665+
requests made without it will fail (unless you specify a `baseUrl` to a proxy
666+
that inserts a valid API key on your behalf).
667+
668+
**Kind**: global function
669+
**Returns**: <code>Promise.&lt;Array.&lt;string&gt;&gt;</code> \| <code>Promise.&lt;null&gt;</code> - a Promise which resolves to an
670+
array of email addresses (or null if no results were found), or rejects with
671+
an Error
672+
673+
| Param | Type | Description |
674+
| --- | --- | --- |
675+
| websiteDomain | <code>string</code> | the website domain to query (e.g., "example.com") |
676+
| [options] | <code>object</code> | a configuration object |
677+
| [options.apiKey] | <code>string</code> | an API key from https://haveibeenpwned.com/API/Key (default: undefined) |
678+
| [options.baseUrl] | <code>string</code> | a custom base URL for the haveibeenpwned.com API endpoints (default: `https://haveibeenpwned.com/api/v3`) |
679+
| [options.timeoutMs] | <code>number</code> | timeout for the request in milliseconds (default: none) |
680+
| [options.userAgent] | <code>string</code> | a custom string to send as the User-Agent field in the request headers (default: `hibp <version>`) |
681+
682+
**Example**
683+
```js
684+
try {
685+
const data = await stealerLogsByWebsiteDomain("example.com", { apiKey: "my-api-key" });
686+
if (data) {
687+
// ["andy@gmail.com", "jane@gmail.com"]
688+
} else {
689+
// no results
690+
}
691+
} catch (err) {
692+
// ...
693+
}
694+
```
695+
**Example**
696+
```js
697+
try {
698+
const data = await stealerLogsByWebsiteDomain("example.com", {
699+
baseUrl: "https://my-hibp-proxy:8080",
700+
});
701+
if (data) {
702+
// ...
703+
} else {
704+
// ...
705+
}
706+
} catch (err) {
707+
// ...
708+
}
709+
```
644710
<a name="subscribedDomains"></a>
645711

646712
## subscribedDomains([options]) ⇒ <code><a href="#subscribeddomain--object">Promise.&lt;Array.&lt;SubscribedDomain&gt;&gt;</a></code>

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ The following modules are available:
7575
- [pwnedPasswordRange](API.md#pwnedpasswordrange)
7676
- [search](API.md#search)
7777
- [stealerLogsByEmail](API.md#stealerlogsbyemail)
78+
- [stealerLogsByWebsiteDomain](API.md#stealerlogsbywebsitedomain)
7879
- [subscribedDomains](API.md#subscribeddomains)
7980
- [subscriptionStatus](API.md#subscriptionstatus)
8081

src/__tests__/hibp.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ describe('hibp', () => {
1717
"pwnedPasswordRange": [Function],
1818
"search": [Function],
1919
"stealerLogsByEmail": [Function],
20+
"stealerLogsByWebsiteDomain": [Function],
2021
"subscribedDomains": [Function],
2122
"subscriptionStatus": [Function],
2223
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { http } from 'msw';
3+
import { server } from '../../mocks/server.js';
4+
import { NOT_FOUND } from '../api/haveibeenpwned/responses.js';
5+
import { stealerLogsByWebsiteDomain } from '../stealer-logs-by-website-domain.js';
6+
7+
describe('stealerLogsByWebsiteDomain', () => {
8+
const RESULTS = ['andy@gmail.com', 'jane@gmail.com'];
9+
10+
describe('found', () => {
11+
it('resolves with data from the remote API', () => {
12+
server.use(
13+
http.get('*', () => {
14+
return new Response(JSON.stringify(RESULTS));
15+
}),
16+
);
17+
18+
return expect(stealerLogsByWebsiteDomain('example.com', { apiKey: 'k' })).resolves.toEqual(
19+
RESULTS,
20+
);
21+
});
22+
});
23+
24+
describe('not found', () => {
25+
it('resolves with null', () => {
26+
server.use(
27+
http.get('*', () => {
28+
return new Response(null, { status: NOT_FOUND.status });
29+
}),
30+
);
31+
32+
return expect(stealerLogsByWebsiteDomain('example.com')).resolves.toBeNull();
33+
});
34+
});
35+
36+
describe('apiKey option', () => {
37+
it('sets the hibp-api-key header', async () => {
38+
expect.assertions(1);
39+
const apiKey = 'my-api-key';
40+
server.use(
41+
http.get('*', ({ request }) => {
42+
expect(request.headers.get('hibp-api-key')).toBe(apiKey);
43+
return new Response(JSON.stringify(RESULTS));
44+
}),
45+
);
46+
47+
return stealerLogsByWebsiteDomain('example.com', { apiKey });
48+
});
49+
});
50+
51+
describe('baseUrl option', () => {
52+
it('is the beginning of the final URL', () => {
53+
const baseUrl = 'https://my-hibp-proxy:8080';
54+
server.use(
55+
http.get(new RegExp(`^${baseUrl}`), () => {
56+
return new Response(JSON.stringify(RESULTS));
57+
}),
58+
);
59+
60+
return expect(stealerLogsByWebsiteDomain('example.com', { baseUrl })).resolves.toEqual(
61+
RESULTS,
62+
);
63+
});
64+
});
65+
66+
describe('timeoutMs option', () => {
67+
it('aborts the request after the given value', () => {
68+
expect.assertions(1);
69+
const timeoutMs = 1;
70+
server.use(
71+
http.get('*', async () => {
72+
await new Promise((resolve) => {
73+
setTimeout(resolve, timeoutMs + 1);
74+
});
75+
return new Response(JSON.stringify(RESULTS));
76+
}),
77+
);
78+
79+
return expect(
80+
stealerLogsByWebsiteDomain('example.com', { timeoutMs }),
81+
).rejects.toMatchInlineSnapshot(`[TimeoutError: The operation was aborted due to timeout]`);
82+
});
83+
});
84+
85+
describe('userAgent option', () => {
86+
it('is passed on as a request header', () => {
87+
expect.assertions(1);
88+
const userAgent = 'Custom UA';
89+
server.use(
90+
http.get('*', ({ request }) => {
91+
expect(request.headers.get('User-Agent')).toBe(userAgent);
92+
return new Response(JSON.stringify(RESULTS));
93+
}),
94+
);
95+
96+
return stealerLogsByWebsiteDomain('example.com', { userAgent });
97+
});
98+
});
99+
});

src/hibp.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { pwnedPassword } from './pwned-password.js';
1010
import { pwnedPasswordRange } from './pwned-password-range.js';
1111
import { search } from './search.js';
1212
import { stealerLogsByEmail } from './stealer-logs-by-email.js';
13+
import { stealerLogsByWebsiteDomain } from './stealer-logs-by-website-domain.js';
1314
import { subscribedDomains } from './subscribed-domains.js';
1415
import { subscriptionStatus } from './subscription-status.js';
1516

@@ -36,6 +37,7 @@ export {
3637
pwnedPasswordRange,
3738
search,
3839
stealerLogsByEmail,
40+
stealerLogsByWebsiteDomain,
3941
subscribedDomains,
4042
subscriptionStatus,
4143
RateLimitError,
@@ -54,6 +56,7 @@ export interface HIBP {
5456
pwnedPasswordRange: typeof pwnedPasswordRange;
5557
search: typeof search;
5658
stealerLogsByEmail: typeof stealerLogsByEmail;
59+
stealerLogsByWebsiteDomain: typeof stealerLogsByWebsiteDomain;
5760
subscribedDomains: typeof subscribedDomains;
5861
subscriptionStatus: typeof subscriptionStatus;
5962
RateLimitError: typeof RateLimitError;
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { fetchFromApi } from './api/haveibeenpwned/fetch-from-api.js';
2+
3+
/**
4+
* Fetches all stealer log email addresses for a website domain.
5+
*
6+
* The result is an array of strings representing email addresses found in
7+
* stealer logs for the specified website domain (e.g., "example.com").
8+
*
9+
* 🔑 `haveibeenpwned.com` requires an API key from
10+
* https://haveibeenpwned.com/API/Key for the `stealerlogsbywebsitedomain`
11+
* endpoint. The `apiKey` option here is not explicitly required, but direct
12+
* requests made without it will fail (unless you specify a `baseUrl` to a proxy
13+
* that inserts a valid API key on your behalf).
14+
*
15+
* @param {string} websiteDomain the website domain to query (e.g., "example.com")
16+
* @param {object} [options] a configuration object
17+
* @param {string} [options.apiKey] an API key from
18+
* https://haveibeenpwned.com/API/Key (default: undefined)
19+
* @param {string} [options.baseUrl] a custom base URL for the
20+
* haveibeenpwned.com API endpoints (default:
21+
* `https://haveibeenpwned.com/api/v3`)
22+
* @param {number} [options.timeoutMs] timeout for the request in milliseconds
23+
* (default: none)
24+
* @param {string} [options.userAgent] a custom string to send as the User-Agent
25+
* field in the request headers (default: `hibp <version>`)
26+
* @returns {(Promise<string[]> | Promise<null>)} a Promise which resolves to an
27+
* array of email addresses (or null if no results were found), or rejects with
28+
* an Error
29+
* @example
30+
* try {
31+
* const data = await stealerLogsByWebsiteDomain("example.com", { apiKey: "my-api-key" });
32+
* if (data) {
33+
* // ["andy@gmail.com", "jane@gmail.com"]
34+
* } else {
35+
* // no results
36+
* }
37+
* } catch (err) {
38+
* // ...
39+
* }
40+
* @example
41+
* try {
42+
* const data = await stealerLogsByWebsiteDomain("example.com", {
43+
* baseUrl: "https://my-hibp-proxy:8080",
44+
* });
45+
* if (data) {
46+
* // ...
47+
* } else {
48+
* // ...
49+
* }
50+
* } catch (err) {
51+
* // ...
52+
* }
53+
*/
54+
export function stealerLogsByWebsiteDomain(
55+
websiteDomain: string,
56+
options: {
57+
/**
58+
* an API key from https://haveibeenpwned.com/API/Key (default: undefined)
59+
*/
60+
apiKey?: string;
61+
/**
62+
* a custom base URL for the haveibeenpwned.com API endpoints (default:
63+
* `https://haveibeenpwned.com/api/v3`)
64+
*/
65+
baseUrl?: string;
66+
/**
67+
* timeout for the request in milliseconds (default: none)
68+
*/
69+
timeoutMs?: number;
70+
/**
71+
* a custom string to send as the User-Agent field in the request headers
72+
* (default: `hibp <version>`)
73+
*/
74+
userAgent?: string;
75+
} = {},
76+
): Promise<string[] | null> {
77+
const { apiKey, baseUrl, timeoutMs, userAgent } = options;
78+
const endpoint = `/stealerlogsbywebsitedomain/${encodeURIComponent(websiteDomain)}`;
79+
80+
return fetchFromApi(endpoint, {
81+
apiKey,
82+
baseUrl,
83+
timeoutMs,
84+
userAgent,
85+
}) as Promise<string[] | null>;
86+
}

0 commit comments

Comments
 (0)