@hasna/connectors 1.3.45 → 1.3.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/bin/index.js +792 -130
- package/bin/mcp.js +438 -52
- package/bin/serve.js +438 -52
- package/connectors/cloudflare-workers/.env.example +5 -0
- package/connectors/cloudflare-workers/AGENTS.md +11 -0
- package/connectors/cloudflare-workers/CLAUDE.md +11 -0
- package/connectors/cloudflare-workers/README.md +45 -0
- package/connectors/cloudflare-workers/package.json +47 -0
- package/connectors/cloudflare-workers/src/api/client.test.ts +144 -0
- package/connectors/cloudflare-workers/src/api/client.ts +386 -0
- package/connectors/cloudflare-workers/src/api/index.ts +1 -0
- package/connectors/cloudflare-workers/src/cli/index.ts +206 -0
- package/connectors/cloudflare-workers/src/index.ts +16 -0
- package/connectors/cloudflare-workers/src/types/index.ts +53 -0
- package/connectors/cloudflare-workers/src/utils/config.ts +61 -0
- package/connectors/cloudflare-workers/src/utils/output.ts +26 -0
- package/connectors/cloudflare-workers/tsconfig.build.json +4 -0
- package/connectors/cloudflare-workers/tsconfig.json +14 -0
- package/connectors/tidio/.env.example +3 -2
- package/connectors/tidio/CLAUDE.md +21 -96
- package/connectors/tidio/README.md +22 -18
- package/connectors/tidio/package.json +4 -2
- package/connectors/tidio/src/api/client.ts +26 -15
- package/connectors/tidio/src/api/index.ts +185 -128
- package/connectors/tidio/src/api/tidio.test.ts +175 -44
- package/connectors/tidio/src/cli/index.ts +154 -388
- package/connectors/tidio/src/index.ts +5 -3
- package/connectors/tidio/src/types/index.ts +135 -82
- package/connectors/tidio/src/utils/config.ts +90 -44
- package/connectors/wandb/.env.example +7 -0
- package/connectors/wandb/AGENTS.md +43 -0
- package/connectors/wandb/CLAUDE.md +67 -0
- package/connectors/wandb/README.md +65 -0
- package/connectors/wandb/package.json +52 -0
- package/connectors/wandb/src/api/client.test.ts +116 -0
- package/connectors/wandb/src/api/client.ts +83 -0
- package/connectors/wandb/src/api/graphql.ts +9 -0
- package/connectors/wandb/src/api/index.ts +36 -0
- package/connectors/wandb/src/api/projects.ts +36 -0
- package/connectors/wandb/src/api/viewer.ts +20 -0
- package/connectors/wandb/src/cli/index.ts +265 -0
- package/connectors/wandb/src/index.ts +22 -0
- package/connectors/wandb/src/types/index.ts +75 -0
- package/connectors/wandb/src/utils/config.ts +164 -0
- package/connectors/wandb/src/utils/output.ts +119 -0
- package/connectors/wandb/tsconfig.json +16 -0
- package/connectors/weights-biases/.env.example +7 -0
- package/connectors/weights-biases/CLAUDE.md +65 -0
- package/connectors/weights-biases/README.md +62 -0
- package/connectors/weights-biases/package.json +53 -0
- package/connectors/weights-biases/src/api/client.test.ts +46 -0
- package/connectors/weights-biases/src/api/client.ts +119 -0
- package/connectors/weights-biases/src/api/events.ts +18 -0
- package/connectors/weights-biases/src/api/index.ts +56 -0
- package/connectors/weights-biases/src/api/runs.ts +28 -0
- package/connectors/weights-biases/src/api/search.ts +10 -0
- package/connectors/weights-biases/src/cli/index.ts +289 -0
- package/connectors/weights-biases/src/index.ts +19 -0
- package/connectors/weights-biases/src/types/index.ts +92 -0
- package/connectors/weights-biases/src/utils/config.ts +168 -0
- package/connectors/weights-biases/src/utils/output.ts +116 -0
- package/connectors/weights-biases/tsconfig.json +16 -0
- package/connectors/zoominfo/.env.example +4 -0
- package/connectors/zoominfo/AGENTS.md +26 -0
- package/connectors/zoominfo/CLAUDE.md +20 -0
- package/connectors/zoominfo/README.md +69 -0
- package/connectors/zoominfo/package.json +52 -0
- package/connectors/zoominfo/src/api/client.test.ts +140 -0
- package/connectors/zoominfo/src/api/client.ts +228 -0
- package/connectors/zoominfo/src/api/index.ts +1 -0
- package/connectors/zoominfo/src/cli/index.ts +179 -0
- package/connectors/zoominfo/src/index.ts +3 -0
- package/connectors/zoominfo/src/types/index.ts +39 -0
- package/connectors/zoominfo/src/utils/config.ts +61 -0
- package/connectors/zoominfo/src/utils/output.ts +27 -0
- package/connectors/zoominfo/tsconfig.json +20 -0
- package/connectors/zotero/.env.example +7 -0
- package/connectors/zotero/.npmrc.example +2 -0
- package/connectors/zotero/AGENTS.md +40 -0
- package/connectors/zotero/CLAUDE.md +69 -0
- package/connectors/zotero/README.md +91 -0
- package/connectors/zotero/package.json +43 -0
- package/connectors/zotero/src/api/attachments.ts +130 -0
- package/connectors/zotero/src/api/client.test.ts +166 -0
- package/connectors/zotero/src/api/client.ts +169 -0
- package/connectors/zotero/src/api/collections.ts +26 -0
- package/connectors/zotero/src/api/index.ts +45 -0
- package/connectors/zotero/src/api/items.ts +64 -0
- package/connectors/zotero/src/cli/index.ts +386 -0
- package/connectors/zotero/src/index.ts +28 -0
- package/connectors/zotero/src/types/index.ts +129 -0
- package/connectors/zotero/src/utils/config.ts +245 -0
- package/connectors/zotero/src/utils/output.ts +119 -0
- package/connectors/zotero/tsconfig.json +16 -0
- package/connectors/zymbly/.env.example +2 -0
- package/connectors/zymbly/AGENTS.md +11 -0
- package/connectors/zymbly/CLAUDE.md +5 -0
- package/connectors/zymbly/README.md +35 -0
- package/connectors/zymbly/package.json +52 -0
- package/connectors/zymbly/src/api/client.test.ts +110 -0
- package/connectors/zymbly/src/api/client.ts +166 -0
- package/connectors/zymbly/src/api/index.ts +1 -0
- package/connectors/zymbly/src/cli/index.ts +156 -0
- package/connectors/zymbly/src/index.ts +12 -0
- package/connectors/zymbly/src/types/index.ts +34 -0
- package/connectors/zymbly/src/utils/config.ts +52 -0
- package/connectors/zymbly/src/utils/output.ts +17 -0
- package/connectors/zymbly/tsconfig.json +13 -0
- package/dist/index.js +45 -2
- package/dist/lib/compact-output.d.ts +40 -0
- package/dist/mcp/tools/jobs.d.ts +32 -0
- package/dist/mcp/tools/jobs.test.d.ts +1 -0
- package/package.json +3 -2
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Guidance for AI agents working on this connector.
|
|
4
|
+
|
|
5
|
+
## Scope
|
|
6
|
+
|
|
7
|
+
This package implements `@hasna/connect-zoominfo`, a real ZoomInfo B2B sales intelligence REST API connector. Keep it on documented ZoomInfo HTTP endpoints and do not add browser automation dependencies.
|
|
8
|
+
|
|
9
|
+
## API Model
|
|
10
|
+
|
|
11
|
+
- Default base URL: `https://api.zoominfo.com`
|
|
12
|
+
- Authentication: `POST /authenticate` with username/password returns a JWT, or use a preconfigured JWT via `ZOOMINFO_JWT`
|
|
13
|
+
- Subsequent requests use `Authorization: Bearer <jwt>`
|
|
14
|
+
- Raw requests must use relative paths without `..` traversal
|
|
15
|
+
- Keep `.env.example` values as placeholders only
|
|
16
|
+
|
|
17
|
+
## Checks
|
|
18
|
+
|
|
19
|
+
Before finishing changes:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bun run typecheck
|
|
23
|
+
bun run build
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Also scan for committed secrets, internal references, and browser automation dependencies.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
See `AGENTS.md` for connector-specific instructions.
|
|
4
|
+
|
|
5
|
+
## Authentication
|
|
6
|
+
|
|
7
|
+
ZoomInfo uses username/password authentication via `POST /authenticate` to obtain a Bearer JWT token. Preconfigured JWT tokens are also supported via `ZOOMINFO_JWT` or `connect-zoominfo config set-jwt`.
|
|
8
|
+
|
|
9
|
+
Subsequent API calls use `Authorization: Bearer <jwt>`.
|
|
10
|
+
|
|
11
|
+
Environment variables:
|
|
12
|
+
|
|
13
|
+
| Variable | Description |
|
|
14
|
+
|----------|-------------|
|
|
15
|
+
| `ZOOMINFO_USERNAME` | ZoomInfo API username |
|
|
16
|
+
| `ZOOMINFO_PASSWORD` | ZoomInfo API password |
|
|
17
|
+
| `ZOOMINFO_JWT` | Preconfigured Bearer JWT (skips /authenticate) |
|
|
18
|
+
| `ZOOMINFO_BASE_URL` | Override API base URL |
|
|
19
|
+
|
|
20
|
+
JWT tokens expire after approximately 60 minutes. Long-running processes should re-authenticate or refresh the configured JWT.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# @hasna/connect-zoominfo
|
|
2
|
+
|
|
3
|
+
TypeScript and CLI connector for the ZoomInfo B2B sales intelligence API.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @hasna/connect-zoominfo
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Configure
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
export ZOOMINFO_USERNAME=your_zoominfo_username
|
|
15
|
+
export ZOOMINFO_PASSWORD=your_zoominfo_password
|
|
16
|
+
export ZOOMINFO_JWT=your_zoominfo_jwt
|
|
17
|
+
export ZOOMINFO_BASE_URL=https://api.zoominfo.com
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The CLI can also store local configuration:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
connect-zoominfo config set-username your_zoominfo_username
|
|
24
|
+
connect-zoominfo config set-password your_zoominfo_password
|
|
25
|
+
connect-zoominfo config set-jwt your_zoominfo_jwt
|
|
26
|
+
connect-zoominfo config show
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## CLI Examples
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
connect-zoominfo authenticate
|
|
33
|
+
connect-zoominfo search-contacts --body '{"jobTitle":["VP of Sales"],"rpp":25,"page":1}'
|
|
34
|
+
connect-zoominfo search-companies --body '{"companyName":"Example Corp"}'
|
|
35
|
+
connect-zoominfo enrich-contact --body '{"matchPersonInput":[{"emailAddress":"person@example.com"}],"outputFields":["email","jobTitle"]}'
|
|
36
|
+
connect-zoominfo enrich-company --body '{"matchCompanyInput":[{"companyName":"Example Corp"}]}'
|
|
37
|
+
connect-zoominfo lookup-contact 123456789
|
|
38
|
+
connect-zoominfo contact-search-output-fields
|
|
39
|
+
connect-zoominfo raw-request POST /search/contact --body '{"jobTitle":["CTO"]}'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## TypeScript
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { ZoomInfo } from "@hasna/connect-zoominfo";
|
|
46
|
+
|
|
47
|
+
const zoominfo = new ZoomInfo({
|
|
48
|
+
username: process.env.ZOOMINFO_USERNAME,
|
|
49
|
+
password: process.env.ZOOMINFO_PASSWORD,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await zoominfo.authenticate();
|
|
53
|
+
const contacts = await zoominfo.searchContacts({ jobTitle: ["VP of Sales"], rpp: 25, page: 1 });
|
|
54
|
+
const enriched = await zoominfo.enrichContact({
|
|
55
|
+
matchPersonInput: [{ emailAddress: "person@example.com" }],
|
|
56
|
+
outputFields: ["email", "jobTitle"],
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## API Coverage
|
|
61
|
+
|
|
62
|
+
- Username/password authentication (`POST /authenticate`) with JWT caching per client instance
|
|
63
|
+
- Contact and company search
|
|
64
|
+
- Contact and company enrichment
|
|
65
|
+
- Contact lookup by ID
|
|
66
|
+
- Search output field discovery
|
|
67
|
+
- Relative raw requests
|
|
68
|
+
|
|
69
|
+
JWT tokens are valid for approximately 60 minutes. Long-running processes may need to re-authenticate or provide a fresh `ZOOMINFO_JWT`.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hasna/connect-zoominfo",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "ZoomInfo B2B sales intelligence API connector for contact and company search, enrichment, and lookup",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"connect-zoominfo": "./bin/index.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "bun build ./src/index.ts --outdir ./dist --target bun && bun build ./src/cli/index.ts --outdir ./bin --target bun",
|
|
19
|
+
"dev": "bun run ./src/cli/index.ts",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"prepublishOnly": "bun run build"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"zoominfo",
|
|
25
|
+
"sales-intelligence",
|
|
26
|
+
"enrichment",
|
|
27
|
+
"contacts",
|
|
28
|
+
"companies",
|
|
29
|
+
"b2b",
|
|
30
|
+
"api",
|
|
31
|
+
"connector",
|
|
32
|
+
"cli",
|
|
33
|
+
"typescript",
|
|
34
|
+
"bun"
|
|
35
|
+
],
|
|
36
|
+
"author": "Hasna",
|
|
37
|
+
"license": "Apache-2.0",
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/bun": "latest",
|
|
40
|
+
"typescript": "^5"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"commander": "^12.1.0"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"bun": ">=1.0.0"
|
|
47
|
+
},
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/hasna/connect-zoominfo.git"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
import { ZoomInfo, ZoomInfoApiError } from "../index";
|
|
3
|
+
|
|
4
|
+
const originalFetch = globalThis.fetch;
|
|
5
|
+
let fetchMock: ReturnType<typeof mock>;
|
|
6
|
+
|
|
7
|
+
function mockJsonResponse(body: unknown, init: ResponseInit = {}) {
|
|
8
|
+
return new Response(JSON.stringify(body), {
|
|
9
|
+
status: init.status ?? 200,
|
|
10
|
+
headers: { "content-type": "application/json", ...init.headers },
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function clientWithCredentials() {
|
|
15
|
+
return new ZoomInfo({
|
|
16
|
+
username: "api-user",
|
|
17
|
+
password: "api-pass",
|
|
18
|
+
baseUrl: "https://api.zoominfo.com",
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function clientWithJwt() {
|
|
23
|
+
return new ZoomInfo({
|
|
24
|
+
jwt: "configured-jwt-token",
|
|
25
|
+
baseUrl: "https://custom.zoominfo.test",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
fetchMock = mock(async () => mockJsonResponse({ success: true }));
|
|
31
|
+
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
globalThis.fetch = originalFetch;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("ZoomInfoClient", () => {
|
|
39
|
+
test("authenticates with username/password and caches JWT for subsequent calls", async () => {
|
|
40
|
+
fetchMock = mock(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
41
|
+
const url = String(input);
|
|
42
|
+
if (url.endsWith("/authenticate")) {
|
|
43
|
+
return mockJsonResponse({ jwt: "fresh-jwt-token" });
|
|
44
|
+
}
|
|
45
|
+
if (url.endsWith("/search/contact")) {
|
|
46
|
+
const headers = init?.headers as Record<string, string>;
|
|
47
|
+
expect(headers.Authorization).toBe("Bearer fresh-jwt-token");
|
|
48
|
+
return mockJsonResponse({ data: [{ id: "1" }] });
|
|
49
|
+
}
|
|
50
|
+
return mockJsonResponse({});
|
|
51
|
+
});
|
|
52
|
+
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
|
53
|
+
|
|
54
|
+
const api = clientWithCredentials();
|
|
55
|
+
const auth = await api.authenticate();
|
|
56
|
+
expect(auth.jwt).toBe("fresh-jwt-token");
|
|
57
|
+
|
|
58
|
+
await api.searchContacts({ jobTitle: ["VP of Sales"] });
|
|
59
|
+
|
|
60
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
61
|
+
const [authUrl, authInit] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
62
|
+
expect(authUrl).toBe("https://api.zoominfo.com/authenticate");
|
|
63
|
+
expect(authInit.method).toBe("POST");
|
|
64
|
+
expect(JSON.parse(String(authInit.body))).toEqual({ username: "api-user", password: "api-pass" });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("configured JWT skips /authenticate on API calls", async () => {
|
|
68
|
+
const api = clientWithJwt();
|
|
69
|
+
|
|
70
|
+
await api.searchCompanies({ companyName: "Example Corp" });
|
|
71
|
+
|
|
72
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
73
|
+
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
74
|
+
expect(url).toBe("https://custom.zoominfo.test/search/company");
|
|
75
|
+
expect(init.method).toBe("POST");
|
|
76
|
+
expect((init.headers as Record<string, string>).Authorization).toBe("Bearer configured-jwt-token");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("lookupContact uses bearer auth and encodes contact id", async () => {
|
|
80
|
+
const api = clientWithJwt();
|
|
81
|
+
|
|
82
|
+
await api.lookupContact("contact/id");
|
|
83
|
+
|
|
84
|
+
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
85
|
+
expect(url).toBe("https://custom.zoominfo.test/lookup/contact/contact%2Fid");
|
|
86
|
+
expect(init.method).toBe("GET");
|
|
87
|
+
expect((init.headers as Record<string, string>).Authorization).toBe("Bearer configured-jwt-token");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("enrichContact requires matchPersonInput", async () => {
|
|
91
|
+
const api = clientWithJwt();
|
|
92
|
+
|
|
93
|
+
await expect(api.enrichContact({ outputFields: ["email"] })).rejects.toThrow("matchPersonInput");
|
|
94
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("enrichContact sends matchPersonInput payload", async () => {
|
|
98
|
+
const api = clientWithJwt();
|
|
99
|
+
const body = {
|
|
100
|
+
matchPersonInput: [{ emailAddress: "person@example.com" }],
|
|
101
|
+
outputFields: ["email", "jobTitle"],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
await api.enrichContact(body);
|
|
105
|
+
|
|
106
|
+
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
107
|
+
expect(init.method).toBe("POST");
|
|
108
|
+
expect(JSON.parse(String(init.body))).toEqual(body);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("lists contact and company search output fields", async () => {
|
|
112
|
+
const api = clientWithJwt();
|
|
113
|
+
|
|
114
|
+
await api.listContactSearchOutputFields();
|
|
115
|
+
await api.listCompanySearchOutputFields();
|
|
116
|
+
|
|
117
|
+
const urls = (fetchMock.mock.calls as [string, RequestInit][]).map(([url]) => url);
|
|
118
|
+
expect(urls).toEqual([
|
|
119
|
+
"https://custom.zoominfo.test/lookup/outputfields/contact/search",
|
|
120
|
+
"https://custom.zoominfo.test/lookup/outputfields/company/search",
|
|
121
|
+
]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("rawRequest rejects absolute URLs and path traversal", async () => {
|
|
125
|
+
const api = clientWithJwt();
|
|
126
|
+
|
|
127
|
+
await expect(api.rawRequest("GET", "https://evil.example/search/contact")).rejects.toThrow("absolute URLs");
|
|
128
|
+
await expect(api.rawRequest("GET", "/search/../admin")).rejects.toThrow("path traversal");
|
|
129
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("throws ZoomInfoApiError on HTTP failures", async () => {
|
|
133
|
+
fetchMock = mock(async () => mockJsonResponse({ message: "Unauthorized" }, { status: 401 }));
|
|
134
|
+
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
|
135
|
+
|
|
136
|
+
const api = clientWithJwt();
|
|
137
|
+
|
|
138
|
+
await expect(api.searchContacts()).rejects.toBeInstanceOf(ZoomInfoApiError);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import type { HttpMethod, JsonObject, JsonValue, QueryValue, RequestOptions, ZoomInfoConfig } from "../types";
|
|
2
|
+
import { ZoomInfoApiError } from "../types";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_BASE_URL = "https://api.zoominfo.com";
|
|
5
|
+
|
|
6
|
+
export class ZoomInfoClient {
|
|
7
|
+
private readonly username?: string;
|
|
8
|
+
private readonly password?: string;
|
|
9
|
+
private readonly configuredJwt?: string;
|
|
10
|
+
private readonly baseUrl: string;
|
|
11
|
+
private cachedJwt?: string;
|
|
12
|
+
|
|
13
|
+
constructor(config: ZoomInfoConfig = {}) {
|
|
14
|
+
this.username = config.username;
|
|
15
|
+
this.password = config.password;
|
|
16
|
+
this.configuredJwt = config.jwt;
|
|
17
|
+
this.baseUrl = this.cleanBaseUrl(config.baseUrl || DEFAULT_BASE_URL);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async authenticate(): Promise<{ jwt: string }> {
|
|
21
|
+
const username = this.requireString(this.username, "username");
|
|
22
|
+
const password = this.requireString(this.password, "password");
|
|
23
|
+
|
|
24
|
+
const response = await fetch(`${this.baseUrl}/authenticate`, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: {
|
|
27
|
+
Accept: "application/json",
|
|
28
|
+
"Content-Type": "application/json",
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({ username, password }),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const text = await response.text();
|
|
34
|
+
const data = this.parseResponseBody(text, response.headers.get("content-type"));
|
|
35
|
+
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
throw new ZoomInfoApiError(this.errorMessage(data, response.statusText), response.status, data);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const jwt = this.extractJwt(data);
|
|
41
|
+
this.cachedJwt = jwt;
|
|
42
|
+
return { jwt };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async searchContacts(body: JsonValue = {}, params: Record<string, QueryValue> = {}) {
|
|
46
|
+
return this.request("/search/contact", { method: "POST", params, body });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async searchCompanies(body: JsonValue = {}, params: Record<string, QueryValue> = {}) {
|
|
50
|
+
return this.request("/search/company", { method: "POST", params, body });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async enrichContact(body: JsonValue) {
|
|
54
|
+
this.requireMatchPersonInput(body);
|
|
55
|
+
return this.request("/enrich/contact", { method: "POST", body });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async enrichCompany(body: JsonValue = {}) {
|
|
59
|
+
return this.request("/enrich/company", { method: "POST", body });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async lookupContact(contactId: string) {
|
|
63
|
+
return this.request(`/lookup/contact/${this.segment(contactId)}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async listContactSearchOutputFields() {
|
|
67
|
+
return this.request("/lookup/outputfields/contact/search");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async listCompanySearchOutputFields() {
|
|
71
|
+
return this.request("/lookup/outputfields/company/search");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
rawRequest<T = unknown>(method: HttpMethod, path: string, options: Omit<RequestOptions, "method"> = {}) {
|
|
75
|
+
return this.request<T>(path, { ...options, method });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getBaseUrl(): string {
|
|
79
|
+
return this.baseUrl;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async request<T = unknown>(path: string, options: RequestOptions = {}): Promise<T> {
|
|
83
|
+
const method = options.method || "GET";
|
|
84
|
+
const url = this.buildUrl(path, options.params);
|
|
85
|
+
const headers = await this.prepareHeaders(options);
|
|
86
|
+
const body = this.prepareBody(method, options.body, headers);
|
|
87
|
+
|
|
88
|
+
const response = await fetch(url.toString(), {
|
|
89
|
+
method,
|
|
90
|
+
headers,
|
|
91
|
+
body,
|
|
92
|
+
});
|
|
93
|
+
const text = await response.text();
|
|
94
|
+
const data = options.responseType === "text" ? text : this.parseResponseBody(text, response.headers.get("content-type"));
|
|
95
|
+
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
throw new ZoomInfoApiError(this.errorMessage(data, response.statusText), response.status, data);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return data as T;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private async prepareHeaders(options: RequestOptions): Promise<Record<string, string>> {
|
|
104
|
+
const headers: Record<string, string> = {
|
|
105
|
+
Accept: "application/json",
|
|
106
|
+
...options.headers,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
if (options.auth !== false) {
|
|
110
|
+
const jwt = await this.resolveJwt();
|
|
111
|
+
headers.Authorization = `Bearer ${jwt}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return headers;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private async resolveJwt(): Promise<string> {
|
|
118
|
+
if (this.configuredJwt) {
|
|
119
|
+
return this.configuredJwt;
|
|
120
|
+
}
|
|
121
|
+
if (this.cachedJwt) {
|
|
122
|
+
return this.cachedJwt;
|
|
123
|
+
}
|
|
124
|
+
const result = await this.authenticate();
|
|
125
|
+
return result.jwt;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private extractJwt(data: unknown): string {
|
|
129
|
+
if (typeof data === "object" && data !== null) {
|
|
130
|
+
const record = data as Record<string, unknown>;
|
|
131
|
+
for (const key of ["jwt", "access_token", "token"]) {
|
|
132
|
+
const value = record[key];
|
|
133
|
+
if (typeof value === "string" && value.trim()) {
|
|
134
|
+
return value.trim();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
throw new ZoomInfoApiError("ZoomInfo authenticate response did not include a JWT");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private requireMatchPersonInput(body: JsonValue): void {
|
|
142
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
143
|
+
throw new Error("ZoomInfo enrichContact body must be a JSON object with matchPersonInput");
|
|
144
|
+
}
|
|
145
|
+
const matchPersonInput = (body as JsonObject).matchPersonInput;
|
|
146
|
+
if (!Array.isArray(matchPersonInput) || matchPersonInput.length === 0) {
|
|
147
|
+
throw new Error("ZoomInfo enrichContact requires matchPersonInput");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private prepareBody(method: HttpMethod, body: JsonValue | string | undefined, headers: Record<string, string>): string | undefined {
|
|
152
|
+
if (body === undefined || body === null || method === "GET") return undefined;
|
|
153
|
+
headers["Content-Type"] = headers["Content-Type"] || "application/json";
|
|
154
|
+
return typeof body === "string" ? body : JSON.stringify(this.cleanObject(body));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private buildUrl(path: string, params?: Record<string, QueryValue>): URL {
|
|
158
|
+
const trimmedPath = path.trim();
|
|
159
|
+
if (!trimmedPath) throw new Error("ZoomInfo API path is required");
|
|
160
|
+
if (/^https?:\/\//i.test(trimmedPath)) throw new Error("ZoomInfo raw paths must be relative, not absolute URLs");
|
|
161
|
+
if (trimmedPath.includes("..")) throw new Error("ZoomInfo raw paths must not contain path traversal");
|
|
162
|
+
|
|
163
|
+
const normalizedPath = trimmedPath.startsWith("/") ? trimmedPath : `/${trimmedPath}`;
|
|
164
|
+
const url = new URL(`${this.baseUrl}${normalizedPath}`);
|
|
165
|
+
if (params) {
|
|
166
|
+
for (const [key, value] of Object.entries(params)) this.appendQueryValue(url, key, value);
|
|
167
|
+
}
|
|
168
|
+
return url;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private appendQueryValue(url: URL, key: string, value: QueryValue): void {
|
|
172
|
+
if (value === undefined || value === null || value === "") return;
|
|
173
|
+
if (Array.isArray(value)) {
|
|
174
|
+
for (const item of value) url.searchParams.append(key, String(item));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
url.searchParams.append(key, String(value));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private parseResponseBody(text: string, contentType: string | null): unknown {
|
|
181
|
+
if (!text) return {};
|
|
182
|
+
if (contentType?.includes("json") || text.startsWith("{") || text.startsWith("[")) {
|
|
183
|
+
try {
|
|
184
|
+
return JSON.parse(text);
|
|
185
|
+
} catch {
|
|
186
|
+
return text;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return text;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private errorMessage(data: unknown, statusText: string): string {
|
|
193
|
+
if (typeof data === "object" && data !== null) {
|
|
194
|
+
const record = data as Record<string, unknown>;
|
|
195
|
+
if (typeof record.error_message === "string") return record.error_message;
|
|
196
|
+
if (typeof record.error === "string") return record.error;
|
|
197
|
+
if (typeof record.message === "string") return record.message;
|
|
198
|
+
}
|
|
199
|
+
if (typeof data === "string" && data.trim()) return data.slice(0, 500);
|
|
200
|
+
return statusText || "ZoomInfo API request failed";
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private cleanBaseUrl(baseUrl: string): string {
|
|
204
|
+
const trimmed = baseUrl.trim();
|
|
205
|
+
const withProtocol = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
206
|
+
return withProtocol.replace(/\/+$/, "");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private segment(value: string): string {
|
|
210
|
+
return encodeURIComponent(this.requireString(value, "path segment"));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private requireString(value: string | undefined, label: string): string {
|
|
214
|
+
const trimmed = value?.trim();
|
|
215
|
+
if (!trimmed) throw new Error(`ZoomInfo ${label} is required`);
|
|
216
|
+
return trimmed;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private cleanObject(value: JsonValue): JsonValue {
|
|
220
|
+
if (Array.isArray(value)) return value.map((item) => this.cleanObject(item));
|
|
221
|
+
if (typeof value !== "object" || value === null) return value;
|
|
222
|
+
const cleaned: Record<string, JsonValue> = {};
|
|
223
|
+
for (const [key, child] of Object.entries(value)) {
|
|
224
|
+
if (child !== undefined) cleaned[key] = this.cleanObject(child);
|
|
225
|
+
}
|
|
226
|
+
return cleaned;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ZoomInfoClient, ZoomInfoClient as ZoomInfo } from "./client";
|