@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.
Files changed (114) hide show
  1. package/README.md +25 -0
  2. package/bin/index.js +792 -130
  3. package/bin/mcp.js +438 -52
  4. package/bin/serve.js +438 -52
  5. package/connectors/cloudflare-workers/.env.example +5 -0
  6. package/connectors/cloudflare-workers/AGENTS.md +11 -0
  7. package/connectors/cloudflare-workers/CLAUDE.md +11 -0
  8. package/connectors/cloudflare-workers/README.md +45 -0
  9. package/connectors/cloudflare-workers/package.json +47 -0
  10. package/connectors/cloudflare-workers/src/api/client.test.ts +144 -0
  11. package/connectors/cloudflare-workers/src/api/client.ts +386 -0
  12. package/connectors/cloudflare-workers/src/api/index.ts +1 -0
  13. package/connectors/cloudflare-workers/src/cli/index.ts +206 -0
  14. package/connectors/cloudflare-workers/src/index.ts +16 -0
  15. package/connectors/cloudflare-workers/src/types/index.ts +53 -0
  16. package/connectors/cloudflare-workers/src/utils/config.ts +61 -0
  17. package/connectors/cloudflare-workers/src/utils/output.ts +26 -0
  18. package/connectors/cloudflare-workers/tsconfig.build.json +4 -0
  19. package/connectors/cloudflare-workers/tsconfig.json +14 -0
  20. package/connectors/tidio/.env.example +3 -2
  21. package/connectors/tidio/CLAUDE.md +21 -96
  22. package/connectors/tidio/README.md +22 -18
  23. package/connectors/tidio/package.json +4 -2
  24. package/connectors/tidio/src/api/client.ts +26 -15
  25. package/connectors/tidio/src/api/index.ts +185 -128
  26. package/connectors/tidio/src/api/tidio.test.ts +175 -44
  27. package/connectors/tidio/src/cli/index.ts +154 -388
  28. package/connectors/tidio/src/index.ts +5 -3
  29. package/connectors/tidio/src/types/index.ts +135 -82
  30. package/connectors/tidio/src/utils/config.ts +90 -44
  31. package/connectors/wandb/.env.example +7 -0
  32. package/connectors/wandb/AGENTS.md +43 -0
  33. package/connectors/wandb/CLAUDE.md +67 -0
  34. package/connectors/wandb/README.md +65 -0
  35. package/connectors/wandb/package.json +52 -0
  36. package/connectors/wandb/src/api/client.test.ts +116 -0
  37. package/connectors/wandb/src/api/client.ts +83 -0
  38. package/connectors/wandb/src/api/graphql.ts +9 -0
  39. package/connectors/wandb/src/api/index.ts +36 -0
  40. package/connectors/wandb/src/api/projects.ts +36 -0
  41. package/connectors/wandb/src/api/viewer.ts +20 -0
  42. package/connectors/wandb/src/cli/index.ts +265 -0
  43. package/connectors/wandb/src/index.ts +22 -0
  44. package/connectors/wandb/src/types/index.ts +75 -0
  45. package/connectors/wandb/src/utils/config.ts +164 -0
  46. package/connectors/wandb/src/utils/output.ts +119 -0
  47. package/connectors/wandb/tsconfig.json +16 -0
  48. package/connectors/weights-biases/.env.example +7 -0
  49. package/connectors/weights-biases/CLAUDE.md +65 -0
  50. package/connectors/weights-biases/README.md +62 -0
  51. package/connectors/weights-biases/package.json +53 -0
  52. package/connectors/weights-biases/src/api/client.test.ts +46 -0
  53. package/connectors/weights-biases/src/api/client.ts +119 -0
  54. package/connectors/weights-biases/src/api/events.ts +18 -0
  55. package/connectors/weights-biases/src/api/index.ts +56 -0
  56. package/connectors/weights-biases/src/api/runs.ts +28 -0
  57. package/connectors/weights-biases/src/api/search.ts +10 -0
  58. package/connectors/weights-biases/src/cli/index.ts +289 -0
  59. package/connectors/weights-biases/src/index.ts +19 -0
  60. package/connectors/weights-biases/src/types/index.ts +92 -0
  61. package/connectors/weights-biases/src/utils/config.ts +168 -0
  62. package/connectors/weights-biases/src/utils/output.ts +116 -0
  63. package/connectors/weights-biases/tsconfig.json +16 -0
  64. package/connectors/zoominfo/.env.example +4 -0
  65. package/connectors/zoominfo/AGENTS.md +26 -0
  66. package/connectors/zoominfo/CLAUDE.md +20 -0
  67. package/connectors/zoominfo/README.md +69 -0
  68. package/connectors/zoominfo/package.json +52 -0
  69. package/connectors/zoominfo/src/api/client.test.ts +140 -0
  70. package/connectors/zoominfo/src/api/client.ts +228 -0
  71. package/connectors/zoominfo/src/api/index.ts +1 -0
  72. package/connectors/zoominfo/src/cli/index.ts +179 -0
  73. package/connectors/zoominfo/src/index.ts +3 -0
  74. package/connectors/zoominfo/src/types/index.ts +39 -0
  75. package/connectors/zoominfo/src/utils/config.ts +61 -0
  76. package/connectors/zoominfo/src/utils/output.ts +27 -0
  77. package/connectors/zoominfo/tsconfig.json +20 -0
  78. package/connectors/zotero/.env.example +7 -0
  79. package/connectors/zotero/.npmrc.example +2 -0
  80. package/connectors/zotero/AGENTS.md +40 -0
  81. package/connectors/zotero/CLAUDE.md +69 -0
  82. package/connectors/zotero/README.md +91 -0
  83. package/connectors/zotero/package.json +43 -0
  84. package/connectors/zotero/src/api/attachments.ts +130 -0
  85. package/connectors/zotero/src/api/client.test.ts +166 -0
  86. package/connectors/zotero/src/api/client.ts +169 -0
  87. package/connectors/zotero/src/api/collections.ts +26 -0
  88. package/connectors/zotero/src/api/index.ts +45 -0
  89. package/connectors/zotero/src/api/items.ts +64 -0
  90. package/connectors/zotero/src/cli/index.ts +386 -0
  91. package/connectors/zotero/src/index.ts +28 -0
  92. package/connectors/zotero/src/types/index.ts +129 -0
  93. package/connectors/zotero/src/utils/config.ts +245 -0
  94. package/connectors/zotero/src/utils/output.ts +119 -0
  95. package/connectors/zotero/tsconfig.json +16 -0
  96. package/connectors/zymbly/.env.example +2 -0
  97. package/connectors/zymbly/AGENTS.md +11 -0
  98. package/connectors/zymbly/CLAUDE.md +5 -0
  99. package/connectors/zymbly/README.md +35 -0
  100. package/connectors/zymbly/package.json +52 -0
  101. package/connectors/zymbly/src/api/client.test.ts +110 -0
  102. package/connectors/zymbly/src/api/client.ts +166 -0
  103. package/connectors/zymbly/src/api/index.ts +1 -0
  104. package/connectors/zymbly/src/cli/index.ts +156 -0
  105. package/connectors/zymbly/src/index.ts +12 -0
  106. package/connectors/zymbly/src/types/index.ts +34 -0
  107. package/connectors/zymbly/src/utils/config.ts +52 -0
  108. package/connectors/zymbly/src/utils/output.ts +17 -0
  109. package/connectors/zymbly/tsconfig.json +13 -0
  110. package/dist/index.js +45 -2
  111. package/dist/lib/compact-output.d.ts +40 -0
  112. package/dist/mcp/tools/jobs.d.ts +32 -0
  113. package/dist/mcp/tools/jobs.test.d.ts +1 -0
  114. 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";