@chat-ads/chatads-sdk 0.1.0
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/LICENSE +21 -0
- package/README.md +149 -0
- package/dist/examples/basic.d.ts +1 -0
- package/dist/examples/basic.js +22 -0
- package/dist/src/client.d.ts +42 -0
- package/dist/src/client.js +258 -0
- package/dist/src/errors.d.ts +20 -0
- package/dist/src/errors.js +26 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +2 -0
- package/dist/src/models.d.ts +65 -0
- package/dist/src/models.js +18 -0
- package/dist/tests/chatads-client.test.d.ts +1 -0
- package/dist/tests/chatads-client.test.js +7 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 ChatAds
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# ChatAds TypeScript SDK
|
|
2
|
+
|
|
3
|
+
Type-safe client for the ChatAds `/v1/chatads/messages` endpoint. Works in Node.js 18+ (uses the built-in `fetch`) and modern edge runtimes so you can score leads directly from JavaScript, TypeScript, or serverless environments.
|
|
4
|
+
|
|
5
|
+
> **Node 18+ required:** The SDK relies on the built-in `fetch`. For Node 16, pass `fetchImplementation` (e.g. `undici`) or transpile to CJS yourself.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @chat-ads/chatads-sdk
|
|
11
|
+
# or: pnpm add @chat-ads/chatads-sdk
|
|
12
|
+
yarn add @chat-ads/chatads-sdk
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quickstart
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { ChatAdsClient } from "@chat-ads/chatads-sdk";
|
|
19
|
+
|
|
20
|
+
const client = new ChatAdsClient({
|
|
21
|
+
apiKey: process.env.CHATADS_API_KEY!,
|
|
22
|
+
baseUrl: "https://chatads--chatads-api-fastapiserver-serve.modal.run",
|
|
23
|
+
maxRetries: 2,
|
|
24
|
+
raiseOnFailure: true,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const response = await client.analyze({
|
|
28
|
+
message: "Looking for CRM tools for a 10-person sales team",
|
|
29
|
+
ip: "8.8.8.8",
|
|
30
|
+
userAgent: "Mozilla/5.0",
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (response.success && response.data?.ad) {
|
|
34
|
+
console.log(response.data.ad);
|
|
35
|
+
} else {
|
|
36
|
+
console.error(response.error);
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Convenience helper
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
const result = await client.analyzeMessage("Need scheduling ideas", {
|
|
44
|
+
pageUrl: "https://acme.com/contact",
|
|
45
|
+
domain: "acme.com",
|
|
46
|
+
extraFields: {
|
|
47
|
+
formId: "lead-gen-7",
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- Reserved payload keys (like `message`, `pageUrl`, etc.) cannot be overwritten inside `extraFields`; the client throws if it detects a collision.
|
|
53
|
+
- Pass `raiseOnFailure: true` to throw `ChatAdsAPIError` when the API returns `success: false` with HTTP 200 responses.
|
|
54
|
+
- Retries honor `Retry-After` headers and exponential backoff (`maxRetries` + `retryBackoffFactorMs`).
|
|
55
|
+
|
|
56
|
+
## Error handling
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { ChatAdsAPIError, ChatAdsSDKError } from "@chat-ads/chatads-sdk";
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
await client.analyze({ message: "Hi" });
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (error instanceof ChatAdsAPIError) {
|
|
65
|
+
console.error(error.statusCode, error.response?.error);
|
|
66
|
+
} else if (error instanceof ChatAdsSDKError) {
|
|
67
|
+
console.error("Transport/config error", error);
|
|
68
|
+
} else {
|
|
69
|
+
console.error("Unexpected error", error);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`ChatAdsAPIError` wraps non-2xx API responses (and optionally `success:false`). `ChatAdsSDKError` covers local issues like invalid payloads, timeouts, or missing `fetch`.
|
|
75
|
+
|
|
76
|
+
## Configuration options
|
|
77
|
+
|
|
78
|
+
| Option | Description |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| `apiKey` | Required. Value for the `x-api-key` header. |
|
|
81
|
+
| `baseUrl` | Required HTTPS base URL to your ChatAds deployment. |
|
|
82
|
+
| `endpoint` | Defaults to `/v1/chatads/messages`. Override if you host multiple versions. |
|
|
83
|
+
| `timeoutMs` | Request timeout (default 10s). |
|
|
84
|
+
| `maxRetries` | How many automatic retries to attempt for retryable HTTP statuses. |
|
|
85
|
+
| `retryStatuses` | Array of status codes treated as retryable (defaults to `408, 429, 5xx`). |
|
|
86
|
+
| `retryBackoffFactorMs` | Base backoff delay in milliseconds (default 500). |
|
|
87
|
+
| `raiseOnFailure` | Throw when the API returns `success:false` even if HTTP 200. |
|
|
88
|
+
| `fetchImplementation` | Provide a custom `fetch` for Node < 18 or custom runtimes. |
|
|
89
|
+
| `logger` | Optional logger (any object with `debug`). Useful for redacted request logs. |
|
|
90
|
+
|
|
91
|
+
## Response shape
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"success": true,
|
|
96
|
+
"data": {
|
|
97
|
+
"matched": true,
|
|
98
|
+
"ad": {
|
|
99
|
+
"product": "CRM Pro",
|
|
100
|
+
"link": "https://getchatads.com/example",
|
|
101
|
+
"message": "Try CRM Pro for your sales team",
|
|
102
|
+
"category": "Software"
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"error": null,
|
|
106
|
+
"meta": {
|
|
107
|
+
"request_id": "req_123",
|
|
108
|
+
"processing_time_ms": 42.3,
|
|
109
|
+
"usage": {
|
|
110
|
+
"monthly_requests": 120,
|
|
111
|
+
"free_tier_limit": 1000,
|
|
112
|
+
"free_tier_remaining": 880,
|
|
113
|
+
"is_free_tier": false,
|
|
114
|
+
"has_credit_card": true
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
TypeScript projects can import `ChatAdsResponseEnvelope` and related interfaces for full type safety.
|
|
121
|
+
|
|
122
|
+
## Examples
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npm run build
|
|
126
|
+
CHATADS_API_KEY=... CHATADS_BASE_URL=https://chatads--chatads-api-fastapiserver-serve.modal.run \
|
|
127
|
+
node dist/examples/basic.js
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Prefer running the compiled output (after `npm run build`). If you need to run the TypeScript source directly, use `npx ts-node --esm examples/basic.ts` after setting the same env vars.
|
|
131
|
+
|
|
132
|
+
## Development
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
npm install
|
|
136
|
+
npm run lint
|
|
137
|
+
npm test
|
|
138
|
+
npm run build
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
This compiles TypeScript to `dist/` (ESM output). CommonJS consumers should transpile or use dynamic `import()`. Publish the folder via npm (`npm publish`) or reference it locally. Tests/linting can be added via Vitest/ESLint if required.
|
|
142
|
+
|
|
143
|
+
## Changelog
|
|
144
|
+
|
|
145
|
+
See [CHANGELOG.md](./CHANGELOG.md).
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
MIT © ChatAds
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ChatAdsClient } from "../src/index.js";
|
|
2
|
+
async function main() {
|
|
3
|
+
const apiKey = process.env.CHATADS_API_KEY;
|
|
4
|
+
if (!apiKey) {
|
|
5
|
+
throw new Error("CHATADS_API_KEY env var is required");
|
|
6
|
+
}
|
|
7
|
+
const client = new ChatAdsClient({
|
|
8
|
+
apiKey,
|
|
9
|
+
baseUrl: process.env.CHATADS_BASE_URL ?? "https://chatads--chatads-api-fastapiserver-serve.modal.run",
|
|
10
|
+
maxRetries: 1,
|
|
11
|
+
raiseOnFailure: true,
|
|
12
|
+
});
|
|
13
|
+
const response = await client.analyze({
|
|
14
|
+
message: "A great home gym always includes a yoga mat",
|
|
15
|
+
ip: "",
|
|
16
|
+
});
|
|
17
|
+
console.log(JSON.stringify(response, null, 2));
|
|
18
|
+
}
|
|
19
|
+
main().catch((error) => {
|
|
20
|
+
console.error(error);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ChatAdsResponseEnvelope, FunctionItemPayload } from "./models.js";
|
|
2
|
+
type FetchLike = typeof fetch;
|
|
3
|
+
type Logger = Pick<Console, "debug">;
|
|
4
|
+
export interface ChatAdsClientOptions {
|
|
5
|
+
apiKey: string;
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
endpoint?: string;
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
maxRetries?: number;
|
|
10
|
+
retryStatuses?: number[];
|
|
11
|
+
retryBackoffFactorMs?: number;
|
|
12
|
+
raiseOnFailure?: boolean;
|
|
13
|
+
fetchImplementation?: FetchLike;
|
|
14
|
+
logger?: Logger;
|
|
15
|
+
userAgent?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface AnalyzeOptions {
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
headers?: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
export type AnalyzeMessageOptions = Omit<FunctionItemPayload, "message"> & {
|
|
22
|
+
extraFields?: Record<string, unknown>;
|
|
23
|
+
};
|
|
24
|
+
export declare class ChatAdsClient {
|
|
25
|
+
private readonly apiKey;
|
|
26
|
+
private readonly baseUrl;
|
|
27
|
+
private readonly endpoint;
|
|
28
|
+
private readonly timeoutMs;
|
|
29
|
+
private readonly maxRetries;
|
|
30
|
+
private readonly retryStatuses;
|
|
31
|
+
private readonly retryBackoffFactorMs;
|
|
32
|
+
private readonly raiseOnFailure;
|
|
33
|
+
private readonly fetchImpl;
|
|
34
|
+
private readonly logger?;
|
|
35
|
+
private readonly userAgent?;
|
|
36
|
+
constructor(options: ChatAdsClientOptions);
|
|
37
|
+
analyze(payload: FunctionItemPayload, options?: AnalyzeOptions): Promise<ChatAdsResponseEnvelope>;
|
|
38
|
+
analyzeMessage(message: string, extra?: AnalyzeMessageOptions, options?: AnalyzeOptions): Promise<ChatAdsResponseEnvelope>;
|
|
39
|
+
private post;
|
|
40
|
+
private logDebug;
|
|
41
|
+
}
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { ChatAdsAPIError, ChatAdsSDKError } from "./errors.js";
|
|
2
|
+
import { RESERVED_PAYLOAD_KEYS } from "./models.js";
|
|
3
|
+
const DEFAULT_ENDPOINT = "/v1/chatads/messages";
|
|
4
|
+
const DEFAULT_TIMEOUT_MS = 10000;
|
|
5
|
+
const DEFAULT_BACKOFF_FACTOR = 500; // ms
|
|
6
|
+
const DEFAULT_RETRYABLE_STATUSES = new Set([408, 409, 425, 429, 500, 502, 503, 504]);
|
|
7
|
+
const FIELD_ALIASES = {
|
|
8
|
+
pageurl: "pageUrl",
|
|
9
|
+
page_url: "pageUrl",
|
|
10
|
+
pagetitle: "pageTitle",
|
|
11
|
+
page_title: "pageTitle",
|
|
12
|
+
useragent: "userAgent",
|
|
13
|
+
user_agent: "userAgent",
|
|
14
|
+
};
|
|
15
|
+
export class ChatAdsClient {
|
|
16
|
+
constructor(options) {
|
|
17
|
+
if (!options?.apiKey) {
|
|
18
|
+
throw new ChatAdsSDKError("apiKey is required");
|
|
19
|
+
}
|
|
20
|
+
this.apiKey = options.apiKey;
|
|
21
|
+
this.baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
22
|
+
this.endpoint = normalizeEndpoint(options.endpoint ?? DEFAULT_ENDPOINT);
|
|
23
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
24
|
+
this.maxRetries = Math.max(0, options.maxRetries ?? 0);
|
|
25
|
+
this.retryBackoffFactorMs = Math.max(0, options.retryBackoffFactorMs ?? DEFAULT_BACKOFF_FACTOR);
|
|
26
|
+
this.retryStatuses = new Set(options.retryStatuses ?? Array.from(DEFAULT_RETRYABLE_STATUSES));
|
|
27
|
+
this.raiseOnFailure = Boolean(options.raiseOnFailure);
|
|
28
|
+
this.fetchImpl = options.fetchImplementation ?? globalThis.fetch;
|
|
29
|
+
if (!this.fetchImpl) {
|
|
30
|
+
throw new ChatAdsSDKError("Global fetch implementation not found. Pass fetchImplementation explicitly or upgrade to Node 18+");
|
|
31
|
+
}
|
|
32
|
+
this.logger = options.logger;
|
|
33
|
+
this.userAgent = options.userAgent;
|
|
34
|
+
}
|
|
35
|
+
async analyze(payload, options) {
|
|
36
|
+
const body = buildPayload(payload);
|
|
37
|
+
return this.post(body, options);
|
|
38
|
+
}
|
|
39
|
+
async analyzeMessage(message, extra, options) {
|
|
40
|
+
const payload = {
|
|
41
|
+
message,
|
|
42
|
+
...normalizeOptionalFields(extra ?? {}),
|
|
43
|
+
};
|
|
44
|
+
if (extra?.extraFields) {
|
|
45
|
+
payload.extraFields = extra.extraFields;
|
|
46
|
+
}
|
|
47
|
+
return this.analyze(payload, options);
|
|
48
|
+
}
|
|
49
|
+
async post(body, options) {
|
|
50
|
+
const url = `${this.baseUrl}${this.endpoint}`;
|
|
51
|
+
const headers = {
|
|
52
|
+
"content-type": "application/json",
|
|
53
|
+
"x-api-key": this.apiKey,
|
|
54
|
+
...lowercaseHeaders(options?.headers),
|
|
55
|
+
};
|
|
56
|
+
if (this.userAgent) {
|
|
57
|
+
headers["user-agent"] = this.userAgent;
|
|
58
|
+
}
|
|
59
|
+
let attempt = 0;
|
|
60
|
+
// eslint-disable-next-line no-constant-condition
|
|
61
|
+
while (true) {
|
|
62
|
+
const timeout = options?.timeoutMs ?? this.timeoutMs;
|
|
63
|
+
const controller = new AbortController();
|
|
64
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
65
|
+
try {
|
|
66
|
+
this.logDebug(() => ["ChatAds request", { url, headers: sanitizeHeaders(headers), body: sanitizePayload(body) }]);
|
|
67
|
+
const response = await this.fetchImpl(url, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers,
|
|
70
|
+
body: JSON.stringify(body),
|
|
71
|
+
signal: controller.signal,
|
|
72
|
+
});
|
|
73
|
+
const parsed = await parseResponse(response);
|
|
74
|
+
const httpError = !response.ok;
|
|
75
|
+
const logicalError = this.raiseOnFailure && parsed.success === false;
|
|
76
|
+
if (!httpError && !logicalError) {
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
return parsed;
|
|
79
|
+
}
|
|
80
|
+
throw new ChatAdsAPIError({
|
|
81
|
+
statusCode: response.status,
|
|
82
|
+
response: parsed,
|
|
83
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
84
|
+
requestBody: body,
|
|
85
|
+
url,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
const isAbort = error instanceof DOMException && error.name === "AbortError";
|
|
91
|
+
if (isAbort) {
|
|
92
|
+
throw new ChatAdsSDKError(`ChatAds request timed out after ${timeout}ms`, error);
|
|
93
|
+
}
|
|
94
|
+
if (error instanceof ChatAdsAPIError) {
|
|
95
|
+
if (attempt < this.maxRetries && this.retryStatuses.has(error.statusCode)) {
|
|
96
|
+
const delayMs = computeRetryDelay(this.retryBackoffFactorMs, attempt, error.retryAfter);
|
|
97
|
+
await sleep(delayMs);
|
|
98
|
+
attempt += 1;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
if (attempt < this.maxRetries) {
|
|
104
|
+
const delayMs = computeRetryDelay(this.retryBackoffFactorMs, attempt, null);
|
|
105
|
+
await sleep(delayMs);
|
|
106
|
+
attempt += 1;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
throw new ChatAdsSDKError("Unexpected error while calling ChatAds", error);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
logDebug(messageFactory) {
|
|
114
|
+
if (!this.logger?.debug) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const [msg, payload] = messageFactory();
|
|
118
|
+
this.logger.debug(msg, payload);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function normalizeBaseUrl(raw) {
|
|
122
|
+
if (!raw) {
|
|
123
|
+
throw new ChatAdsSDKError("baseUrl is required");
|
|
124
|
+
}
|
|
125
|
+
const trimmed = raw.trim().replace(/\/$/, "");
|
|
126
|
+
try {
|
|
127
|
+
const url = new URL(trimmed);
|
|
128
|
+
if (url.protocol !== "https:") {
|
|
129
|
+
throw new ChatAdsSDKError("baseUrl must start with https://");
|
|
130
|
+
}
|
|
131
|
+
return url.toString().replace(/\/$/, "");
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
throw new ChatAdsSDKError(`Invalid baseUrl: ${raw}`, error);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function normalizeEndpoint(raw) {
|
|
138
|
+
if (!raw.startsWith("/")) {
|
|
139
|
+
return `/${raw}`;
|
|
140
|
+
}
|
|
141
|
+
return raw;
|
|
142
|
+
}
|
|
143
|
+
function buildPayload(payload) {
|
|
144
|
+
if (!payload || typeof payload.message !== "string" || !payload.message.trim()) {
|
|
145
|
+
throw new ChatAdsSDKError("payload.message must be a non-empty string");
|
|
146
|
+
}
|
|
147
|
+
const normalized = {
|
|
148
|
+
message: payload.message.trim(),
|
|
149
|
+
};
|
|
150
|
+
const extra = payload.extraFields ? { ...payload.extraFields } : {};
|
|
151
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
152
|
+
if (key === "message" || key === "extraFields" || value === undefined || value === null) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
normalized[key] = value;
|
|
156
|
+
}
|
|
157
|
+
const conflicts = Object.keys(extra).filter((key) => RESERVED_PAYLOAD_KEYS.has(key));
|
|
158
|
+
if (conflicts.length > 0) {
|
|
159
|
+
throw new ChatAdsSDKError(`extraFields contains reserved keys: ${conflicts.join(", ")}`);
|
|
160
|
+
}
|
|
161
|
+
return { ...normalized, ...extra };
|
|
162
|
+
}
|
|
163
|
+
function normalizeOptionalFields(data) {
|
|
164
|
+
const normalized = {};
|
|
165
|
+
for (const [key, value] of Object.entries(data)) {
|
|
166
|
+
if (value === undefined || value === null) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (key === "extraFields") {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const aliasKey = FIELD_ALIASES[key.toLowerCase()] ?? key;
|
|
173
|
+
normalized[aliasKey] = value;
|
|
174
|
+
}
|
|
175
|
+
return normalized;
|
|
176
|
+
}
|
|
177
|
+
async function parseResponse(response) {
|
|
178
|
+
const text = await response.text();
|
|
179
|
+
try {
|
|
180
|
+
return text ? JSON.parse(text) : { success: false, meta: { request_id: "unknown" } };
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
throw new ChatAdsSDKError("Failed to parse ChatAds response as JSON", error);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function computeRetryDelay(backoffFactor, attempt, retryAfter) {
|
|
187
|
+
const headerDelay = parseRetryAfter(retryAfter);
|
|
188
|
+
if (headerDelay !== null) {
|
|
189
|
+
return headerDelay;
|
|
190
|
+
}
|
|
191
|
+
if (backoffFactor <= 0) {
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
return backoffFactor * 2 ** attempt;
|
|
195
|
+
}
|
|
196
|
+
function parseRetryAfter(value) {
|
|
197
|
+
if (!value) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
const numeric = Number(value);
|
|
201
|
+
if (!Number.isNaN(numeric)) {
|
|
202
|
+
return Math.max(0, numeric * 1000);
|
|
203
|
+
}
|
|
204
|
+
const date = Date.parse(value);
|
|
205
|
+
if (!Number.isNaN(date)) {
|
|
206
|
+
return Math.max(0, date - Date.now());
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
function sleep(durationMs) {
|
|
211
|
+
if (durationMs <= 0) {
|
|
212
|
+
return Promise.resolve();
|
|
213
|
+
}
|
|
214
|
+
return new Promise((resolve) => setTimeout(resolve, durationMs));
|
|
215
|
+
}
|
|
216
|
+
function sanitizePayload(body) {
|
|
217
|
+
return Object.entries(body).reduce((acc, [key, value]) => {
|
|
218
|
+
if (typeof value === "string") {
|
|
219
|
+
acc[key] = { type: "string", length: value.length };
|
|
220
|
+
}
|
|
221
|
+
else if (typeof value === "number") {
|
|
222
|
+
acc[key] = { type: "number" };
|
|
223
|
+
}
|
|
224
|
+
else if (typeof value === "boolean") {
|
|
225
|
+
acc[key] = { type: "boolean" };
|
|
226
|
+
}
|
|
227
|
+
else if (value === null) {
|
|
228
|
+
acc[key] = { type: "null" };
|
|
229
|
+
}
|
|
230
|
+
else if (value === undefined) {
|
|
231
|
+
acc[key] = { type: "undefined" };
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
acc[key] = { type: "object" };
|
|
235
|
+
}
|
|
236
|
+
return acc;
|
|
237
|
+
}, {});
|
|
238
|
+
}
|
|
239
|
+
function sanitizeHeaders(headers) {
|
|
240
|
+
return Object.entries(headers).reduce((acc, [key, value]) => {
|
|
241
|
+
if (key.toLowerCase() === "x-api-key") {
|
|
242
|
+
acc[key] = value.slice(0, 4) + "...";
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
acc[key] = value;
|
|
246
|
+
}
|
|
247
|
+
return acc;
|
|
248
|
+
}, {});
|
|
249
|
+
}
|
|
250
|
+
function lowercaseHeaders(headers) {
|
|
251
|
+
if (!headers) {
|
|
252
|
+
return {};
|
|
253
|
+
}
|
|
254
|
+
return Object.entries(headers).reduce((acc, [key, value]) => {
|
|
255
|
+
acc[key.toLowerCase()] = value;
|
|
256
|
+
return acc;
|
|
257
|
+
}, {});
|
|
258
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ChatAdsResponseEnvelope } from "./models.js";
|
|
2
|
+
export declare class ChatAdsSDKError extends Error {
|
|
3
|
+
cause?: unknown;
|
|
4
|
+
constructor(message: string, cause?: unknown);
|
|
5
|
+
}
|
|
6
|
+
export declare class ChatAdsAPIError extends ChatAdsSDKError {
|
|
7
|
+
readonly statusCode: number;
|
|
8
|
+
readonly response: ChatAdsResponseEnvelope | null;
|
|
9
|
+
readonly headers: Record<string, string>;
|
|
10
|
+
readonly requestBody?: Record<string, unknown>;
|
|
11
|
+
readonly url?: string;
|
|
12
|
+
constructor(params: {
|
|
13
|
+
statusCode: number;
|
|
14
|
+
response: ChatAdsResponseEnvelope | null;
|
|
15
|
+
headers: Record<string, string>;
|
|
16
|
+
requestBody?: Record<string, unknown>;
|
|
17
|
+
url?: string;
|
|
18
|
+
});
|
|
19
|
+
get retryAfter(): string | null;
|
|
20
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export class ChatAdsSDKError extends Error {
|
|
2
|
+
constructor(message, cause) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "ChatAdsSDKError";
|
|
5
|
+
this.cause = cause;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export class ChatAdsAPIError extends ChatAdsSDKError {
|
|
9
|
+
constructor(params) {
|
|
10
|
+
const { statusCode, response } = params;
|
|
11
|
+
const baseMessage = response?.error
|
|
12
|
+
? `${response.error.code}: ${response.error.message}`
|
|
13
|
+
: `HTTP ${statusCode}`;
|
|
14
|
+
super(`ChatAds API error ${statusCode}: ${baseMessage}`);
|
|
15
|
+
this.name = "ChatAdsAPIError";
|
|
16
|
+
this.statusCode = statusCode;
|
|
17
|
+
this.response = response;
|
|
18
|
+
this.headers = params.headers;
|
|
19
|
+
this.requestBody = params.requestBody;
|
|
20
|
+
this.url = params.url;
|
|
21
|
+
}
|
|
22
|
+
get retryAfter() {
|
|
23
|
+
const header = Object.entries(this.headers).find(([key]) => key.toLowerCase() === "retry-after");
|
|
24
|
+
return header ? header[1] : null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { ChatAdsClient } from "./client.js";
|
|
2
|
+
export { ChatAdsAPIError, ChatAdsSDKError } from "./errors.js";
|
|
3
|
+
export type { ChatAdsClientOptions, AnalyzeOptions, AnalyzeMessageOptions, } from "./client.js";
|
|
4
|
+
export type { ChatAdsResponseEnvelope, ChatAdsAd, ChatAdsData, ChatAdsError, ChatAdsMeta, UsageInfo, FunctionItemPayload, FunctionItemOptionalFields, } from "./models.js";
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export type FunctionItemOptionalFields = {
|
|
2
|
+
pageUrl?: string;
|
|
3
|
+
pageTitle?: string;
|
|
4
|
+
referrer?: string;
|
|
5
|
+
address?: string;
|
|
6
|
+
email?: string;
|
|
7
|
+
type?: string;
|
|
8
|
+
domain?: string;
|
|
9
|
+
userAgent?: string;
|
|
10
|
+
ip?: string;
|
|
11
|
+
reason?: string;
|
|
12
|
+
company?: string;
|
|
13
|
+
name?: string;
|
|
14
|
+
country?: string;
|
|
15
|
+
language?: string;
|
|
16
|
+
website?: string;
|
|
17
|
+
};
|
|
18
|
+
export type FunctionItemPayload = {
|
|
19
|
+
message: string;
|
|
20
|
+
extraFields?: Record<string, unknown>;
|
|
21
|
+
} & FunctionItemOptionalFields;
|
|
22
|
+
export interface ChatAdsAd {
|
|
23
|
+
product: string;
|
|
24
|
+
link: string;
|
|
25
|
+
message: string;
|
|
26
|
+
category: string;
|
|
27
|
+
}
|
|
28
|
+
export interface ChatAdsData {
|
|
29
|
+
matched: boolean;
|
|
30
|
+
ad?: ChatAdsAd | null;
|
|
31
|
+
reason?: string | null;
|
|
32
|
+
}
|
|
33
|
+
export interface ChatAdsError {
|
|
34
|
+
code: string;
|
|
35
|
+
message: string;
|
|
36
|
+
details?: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
export interface UsageInfo {
|
|
39
|
+
monthly_requests: number;
|
|
40
|
+
free_tier_limit: number;
|
|
41
|
+
free_tier_remaining: number;
|
|
42
|
+
is_free_tier: boolean;
|
|
43
|
+
has_credit_card: boolean;
|
|
44
|
+
daily_requests?: number | null;
|
|
45
|
+
daily_limit?: number | null;
|
|
46
|
+
minute_requests?: number | null;
|
|
47
|
+
minute_limit?: number | null;
|
|
48
|
+
}
|
|
49
|
+
export interface ChatAdsMeta {
|
|
50
|
+
request_id: string;
|
|
51
|
+
user_id?: string | null;
|
|
52
|
+
country?: string | null;
|
|
53
|
+
language?: string | null;
|
|
54
|
+
processing_time_ms?: number | null;
|
|
55
|
+
usage?: UsageInfo | null;
|
|
56
|
+
[key: string]: unknown;
|
|
57
|
+
}
|
|
58
|
+
export interface ChatAdsResponseEnvelope {
|
|
59
|
+
success: boolean;
|
|
60
|
+
data?: ChatAdsData | null;
|
|
61
|
+
error?: ChatAdsError | null;
|
|
62
|
+
meta: ChatAdsMeta;
|
|
63
|
+
[key: string]: unknown;
|
|
64
|
+
}
|
|
65
|
+
export declare const RESERVED_PAYLOAD_KEYS: ReadonlySet<string>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const RESERVED_PAYLOAD_KEYS = new Set([
|
|
2
|
+
"message",
|
|
3
|
+
"pageUrl",
|
|
4
|
+
"pageTitle",
|
|
5
|
+
"referrer",
|
|
6
|
+
"address",
|
|
7
|
+
"email",
|
|
8
|
+
"type",
|
|
9
|
+
"domain",
|
|
10
|
+
"userAgent",
|
|
11
|
+
"ip",
|
|
12
|
+
"reason",
|
|
13
|
+
"company",
|
|
14
|
+
"name",
|
|
15
|
+
"country",
|
|
16
|
+
"language",
|
|
17
|
+
"website",
|
|
18
|
+
]);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { ChatAdsClient } from "../src/client.js";
|
|
3
|
+
describe("ChatAdsClient", () => {
|
|
4
|
+
test("throws when baseUrl is missing", () => {
|
|
5
|
+
expect(() => new ChatAdsClient({ apiKey: "abc", baseUrl: "" })).toThrowError();
|
|
6
|
+
});
|
|
7
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chat-ads/chatads-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TypeScript/JavaScript client for the ChatAds prospect scoring API",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.json",
|
|
20
|
+
"clean": "rm -rf dist",
|
|
21
|
+
"lint": "tsc --noEmit && eslint 'src/**/*.ts' 'tests/**/*.ts'",
|
|
22
|
+
"test": "vitest run"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"chatads",
|
|
29
|
+
"sdk",
|
|
30
|
+
"typescript",
|
|
31
|
+
"javascript",
|
|
32
|
+
"ads"
|
|
33
|
+
],
|
|
34
|
+
"author": "ChatAds",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/Chat-Ads/chatads-typescript-sdk"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/Chat-Ads/chatads-typescript-sdk/issues"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/Chat-Ads/chatads-typescript-sdk",
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "22.5.0",
|
|
45
|
+
"@typescript-eslint/eslint-plugin": "^7.17.0",
|
|
46
|
+
"@typescript-eslint/parser": "^7.17.0",
|
|
47
|
+
"eslint": "^8.57.0",
|
|
48
|
+
"typescript": "^5.5.4",
|
|
49
|
+
"vitest": "^1.6.0"
|
|
50
|
+
}
|
|
51
|
+
}
|