@zindua/sdk 1.2.6 → 1.2.8
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 +47 -1
- package/dist/client.d.ts +70 -1
- package/dist/client.js +145 -35
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/validate.d.ts +2 -0
- package/dist/validate.js +19 -0
- package/dist/whatsapp-anti-ban.d.ts +7 -0
- package/dist/whatsapp-anti-ban.js +28 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -66,6 +66,24 @@ await zindua.send({
|
|
|
66
66
|
});
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
### WhatsApp anti-ban robot (required)
|
|
70
|
+
|
|
71
|
+
Zindua runs an **anti-ban robot** on WhatsApp sends. Burst / simultaneous OTP traffic looks like spam and can **ban the number you linked** in the dashboard.
|
|
72
|
+
|
|
73
|
+
- **Server:** after a WhatsApp send, wait **≥ 3 seconds** before the next one (same project), or you get `RATE_LIMIT_EXCEEDED` / `Wait Xs before sending another WhatsApp OTP`.
|
|
74
|
+
- **SDK (default):** `autoPaceWhatsapp: true` waits on this client and prints a warning so you learn to space sends in your app (queue, debounce, resend countdown).
|
|
75
|
+
- **Your app:** still use a queue or ≥3s gap for production UX — the SDK cannot pace across multiple servers or cold starts.
|
|
76
|
+
|
|
77
|
+
Guide with Node / PHP / Python / C#: [WhatsApp OTP anti-ban pacing](https://zindua.run/blog/whatsapp-otp-anti-ban-pacing)
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// Default: SDK waits ≥3s between WhatsApp sends on this client
|
|
81
|
+
const zindua = new Zindua({ apiKey: process.env.ZINDUA_API_KEY! });
|
|
82
|
+
|
|
83
|
+
// Only if you already queue WhatsApp OTP yourself:
|
|
84
|
+
new Zindua({ apiKey: process.env.ZINDUA_API_KEY!, autoPaceWhatsapp: false });
|
|
85
|
+
```
|
|
86
|
+
|
|
69
87
|
### Channel and `to` must match
|
|
70
88
|
|
|
71
89
|
The SDK checks **before** calling the API (same rules as [zindua.run](https://zindua.run)). A phone number cannot be sent as email, and an email cannot be sent on WhatsApp.
|
|
@@ -369,6 +387,32 @@ Do **not** embed `@zindua/sdk` or `znd_live_…` in the app. Call your backend r
|
|
|
369
387
|
|
|
370
388
|
---
|
|
371
389
|
|
|
390
|
+
## WordPress site binding & `connect()`
|
|
391
|
+
|
|
392
|
+
Each API key can be linked to **one site URL** (used by the [WordPress plugin](https://zindua.run/wordpress)). Pass `siteUrl` on the client so every request includes `X-Zindua-Site-Url`:
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
const zindua = new Zindua({
|
|
396
|
+
apiKey: process.env.ZINDUA_API_KEY!,
|
|
397
|
+
siteUrl: "https://shop.example.com",
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// First connect binds the key to this URL
|
|
401
|
+
const status = await zindua.connect();
|
|
402
|
+
console.log(status.project.name, status.templates);
|
|
403
|
+
|
|
404
|
+
// Later: list templates with langs (fr, en, …)
|
|
405
|
+
const { templates } = await zindua.getTemplates();
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
| Error code | Meaning |
|
|
409
|
+
|------------|---------|
|
|
410
|
+
| `SITE_ALREADY_BOUND` | Key already used on another site — create a new project or regenerate the key |
|
|
411
|
+
| `SITE_MISMATCH` | `siteUrl` does not match the registered site |
|
|
412
|
+
| `SITE_URL_REQUIRED` | Key is bound; update the SDK and set `siteUrl` |
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
372
416
|
## Configuration options
|
|
373
417
|
|
|
374
418
|
| Option | Default | Description |
|
|
@@ -376,10 +420,12 @@ Do **not** embed `@zindua/sdk` or `znd_live_…` in the app. Call your backend r
|
|
|
376
420
|
| `apiKey` | — | **Required.** `znd_live_…` or `znd_test_…` from the dashboard. |
|
|
377
421
|
| `baseUrl` | `https://zindua.run/api/v1` | Override only for local testing (`http://localhost:3000/api/v1`). |
|
|
378
422
|
| `timeoutMs` | `30000` | Request timeout (max `120000`). |
|
|
423
|
+
| `siteUrl` | — | Your app origin (`https://example.com`). Required once the key is site-bound. |
|
|
379
424
|
|
|
380
425
|
```typescript
|
|
381
426
|
const zindua = new Zindua({
|
|
382
427
|
apiKey: process.env.ZINDUA_API_KEY!,
|
|
428
|
+
siteUrl: process.env.APP_URL,
|
|
383
429
|
baseUrl: process.env.ZINDUA_API_BASE_URL, // optional
|
|
384
430
|
timeoutMs: 45_000,
|
|
385
431
|
});
|
|
@@ -506,7 +552,7 @@ try {
|
|
|
506
552
|
| `WHATSAPP_NOT_CONNECTED` | 422 | WhatsApp not linked. | Dashboard → project → **WhatsApp** → scan QR. |
|
|
507
553
|
| `WHATSAPP_PAUSED` | 422 | WhatsApp session paused. | Resume in the dashboard. |
|
|
508
554
|
| `WHATSAPP_QUOTA_EXCEEDED` | 429 | Monthly WhatsApp limit reached. | Upgrade or wait. |
|
|
509
|
-
| `RATE_LIMIT_EXCEEDED` | 429 |
|
|
555
|
+
| `RATE_LIMIT_EXCEEDED` | 429 | WhatsApp anti-ban robot: sending too fast. | Wait `retryAfterSec` (details), keep ≥3s gap / queue. See [anti-ban guide](https://zindua.run/blog/whatsapp-otp-anti-ban-pacing). |
|
|
510
556
|
| `TEMPLATE_NOT_FOUND` | 404 | Unknown template slug. | Create template or fix slug; check `availableTemplateSlugs` in details. |
|
|
511
557
|
| `TEMPLATE_NO_CONTENT` | 404 | Template has no language version. | Add content in Dashboard → Templates. |
|
|
512
558
|
| `TEMPLATE_NO_WHATSAPP_BODY` | 422 | No plain text for WhatsApp. | Add a text body for that language. |
|
package/dist/client.d.ts
CHANGED
|
@@ -16,6 +16,52 @@ export type ZinduaClientOptions = {
|
|
|
16
16
|
baseUrl?: string;
|
|
17
17
|
/** Request timeout (default 30s, max 120s). */
|
|
18
18
|
timeoutMs?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Your app or WordPress site origin (e.g. https://shop.example.com).
|
|
21
|
+
* Sent as X-Zindua-Site-Url — required once the API key is bound to a site.
|
|
22
|
+
*/
|
|
23
|
+
siteUrl?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Override User-Agent (default Zindua-JS/{version}).
|
|
26
|
+
* Used by @zindua/cli as Zindua-CLI/{version} for admin telemetry.
|
|
27
|
+
*/
|
|
28
|
+
userAgent?: string;
|
|
29
|
+
/**
|
|
30
|
+
* When true (default), the SDK waits ≥3s between WhatsApp sends on this client
|
|
31
|
+
* and logs an anti-ban warning. Mirrors Zindua’s server anti-ban robot.
|
|
32
|
+
* Set false only if your app already queues / debounces WhatsApp OTP.
|
|
33
|
+
*/
|
|
34
|
+
autoPaceWhatsapp?: boolean;
|
|
35
|
+
};
|
|
36
|
+
export type ZinduaProjectInfo = {
|
|
37
|
+
name: string;
|
|
38
|
+
slug: string;
|
|
39
|
+
defaultLang: string;
|
|
40
|
+
dashboardUrl: string;
|
|
41
|
+
};
|
|
42
|
+
export type ZinduaTemplateInfo = {
|
|
43
|
+
slug: string;
|
|
44
|
+
langs: string[];
|
|
45
|
+
defaultLang: string;
|
|
46
|
+
variables: string[];
|
|
47
|
+
};
|
|
48
|
+
export type ZinduaConnectResult = {
|
|
49
|
+
ok: true;
|
|
50
|
+
connected: true;
|
|
51
|
+
siteUrl: string;
|
|
52
|
+
newlyBound: boolean;
|
|
53
|
+
project: ZinduaProjectInfo;
|
|
54
|
+
templates: ZinduaTemplateInfo[];
|
|
55
|
+
plan: ZinduaSendContext["plan"];
|
|
56
|
+
channels: {
|
|
57
|
+
email: {
|
|
58
|
+
ready: boolean;
|
|
59
|
+
};
|
|
60
|
+
whatsapp: {
|
|
61
|
+
ready: boolean;
|
|
62
|
+
status?: string;
|
|
63
|
+
};
|
|
64
|
+
};
|
|
19
65
|
};
|
|
20
66
|
export type ZinduaSendContext = {
|
|
21
67
|
project: {
|
|
@@ -58,10 +104,33 @@ export type ZinduaSendResult = {
|
|
|
58
104
|
};
|
|
59
105
|
export declare class Zindua {
|
|
60
106
|
private readonly apiKey;
|
|
61
|
-
private readonly
|
|
107
|
+
private readonly apiBase;
|
|
62
108
|
private readonly timeoutMs;
|
|
109
|
+
private readonly siteUrl?;
|
|
110
|
+
private readonly userAgent;
|
|
111
|
+
private readonly autoPaceWhatsapp;
|
|
112
|
+
private lastWhatsappSendAt;
|
|
113
|
+
private whatsappPaceChain;
|
|
63
114
|
constructor(options: ZinduaClientOptions);
|
|
64
115
|
/** Returns true when using a znd_test_ key (sandbox / no real delivery). */
|
|
65
116
|
isTestMode(): boolean;
|
|
117
|
+
/**
|
|
118
|
+
* Bind this API key to your site (WordPress connect flow).
|
|
119
|
+
* First call wins — the same key cannot be used on another site URL.
|
|
120
|
+
*/
|
|
121
|
+
connect(siteUrl?: string): Promise<ZinduaConnectResult>;
|
|
122
|
+
/** Project status, channels, and plan (GET /project). */
|
|
123
|
+
getProject(): Promise<Record<string, unknown>>;
|
|
124
|
+
/** Synced templates with langs and variables (GET /templates). */
|
|
125
|
+
getTemplates(): Promise<{
|
|
126
|
+
templates: ZinduaTemplateInfo[];
|
|
127
|
+
limits?: Record<string, unknown>;
|
|
128
|
+
}>;
|
|
129
|
+
/** Delivery status for a logId returned by send() (GET /logs/{logId}). */
|
|
130
|
+
getLog(logId: string): Promise<Record<string, unknown>>;
|
|
66
131
|
send(options: ZinduaSendOptions): Promise<ZinduaSendResult>;
|
|
132
|
+
/** Serialize WhatsApp sends on this client and wait for the anti-ban gap. */
|
|
133
|
+
private paceWhatsappSend;
|
|
134
|
+
private buildHeaders;
|
|
135
|
+
private request;
|
|
67
136
|
}
|
package/dist/client.js
CHANGED
|
@@ -3,7 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.Zindua = void 0;
|
|
4
4
|
const errors_1 = require("./errors");
|
|
5
5
|
const validate_1 = require("./validate");
|
|
6
|
-
const
|
|
6
|
+
const whatsapp_anti_ban_1 = require("./whatsapp-anti-ban");
|
|
7
|
+
const SDK_VERSION = "1.2.8";
|
|
7
8
|
const USER_AGENT = `Zindua-JS/${SDK_VERSION}`;
|
|
8
9
|
function buildPayload(options, channel) {
|
|
9
10
|
const to = (0, validate_1.validateRecipient)(options.to, channel);
|
|
@@ -38,7 +39,7 @@ function buildPayload(options, channel) {
|
|
|
38
39
|
return payload;
|
|
39
40
|
}
|
|
40
41
|
function parseApiError(status, body) {
|
|
41
|
-
|
|
42
|
+
let message = typeof body.error === "string"
|
|
42
43
|
? body.error
|
|
43
44
|
: typeof body.message === "string"
|
|
44
45
|
? body.message
|
|
@@ -50,16 +51,25 @@ function parseApiError(status, body) {
|
|
|
50
51
|
details.hint = body.hint;
|
|
51
52
|
if (typeof body.retryAfterSec === "number")
|
|
52
53
|
details.retryAfterSec = body.retryAfterSec;
|
|
54
|
+
if (typeof body.boundSite === "string")
|
|
55
|
+
details.boundSite = body.boundSite;
|
|
53
56
|
if (Array.isArray(body.availableTemplateSlugs)) {
|
|
54
57
|
details.availableTemplateSlugs = body.availableTemplateSlugs;
|
|
55
58
|
}
|
|
56
59
|
if (body.context && typeof body.context === "object") {
|
|
57
60
|
details.context = body.context;
|
|
58
61
|
}
|
|
59
|
-
const
|
|
62
|
+
const code = typeof body.code === "string" ? body.code : "API_ERROR";
|
|
63
|
+
if (code === "RATE_LIMIT_EXCEEDED" || status === 429) {
|
|
64
|
+
message = (0, whatsapp_anti_ban_1.antiBanRateLimitMessage)(message, typeof body.retryAfterSec === "number" ? body.retryAfterSec : undefined);
|
|
65
|
+
details.antiBanGuide = whatsapp_anti_ban_1.WHATSAPP_ANTI_BAN_GUIDE_URL;
|
|
66
|
+
}
|
|
67
|
+
const fullMessage = typeof body.hint === "string" && body.hint.length > 0 && code !== "RATE_LIMIT_EXCEEDED"
|
|
68
|
+
? `${message} ${body.hint}`
|
|
69
|
+
: message;
|
|
60
70
|
return new errors_1.ZinduaError(fullMessage, {
|
|
61
71
|
status,
|
|
62
|
-
code
|
|
72
|
+
code,
|
|
63
73
|
details: Object.keys(details).length > 0
|
|
64
74
|
? details
|
|
65
75
|
: typeof body.hint === "string"
|
|
@@ -69,34 +79,152 @@ function parseApiError(status, body) {
|
|
|
69
79
|
}
|
|
70
80
|
class Zindua {
|
|
71
81
|
apiKey;
|
|
72
|
-
|
|
82
|
+
apiBase;
|
|
73
83
|
timeoutMs;
|
|
84
|
+
siteUrl;
|
|
85
|
+
userAgent;
|
|
86
|
+
autoPaceWhatsapp;
|
|
87
|
+
lastWhatsappSendAt = 0;
|
|
88
|
+
whatsappPaceChain = Promise.resolve();
|
|
74
89
|
constructor(options) {
|
|
75
90
|
(0, validate_1.assertServerRuntime)();
|
|
76
91
|
this.apiKey = (0, validate_1.validateApiKey)(options.apiKey);
|
|
77
|
-
|
|
78
|
-
this.sendUrl = `${base}/send`;
|
|
92
|
+
this.apiBase = (0, validate_1.resolveBaseUrl)(options.baseUrl);
|
|
79
93
|
this.timeoutMs = (0, validate_1.validateTimeoutMs)(options.timeoutMs);
|
|
94
|
+
this.siteUrl = (0, validate_1.validateSiteUrl)(options.siteUrl);
|
|
95
|
+
this.userAgent =
|
|
96
|
+
typeof options.userAgent === "string" && options.userAgent.trim()
|
|
97
|
+
? options.userAgent.trim()
|
|
98
|
+
: USER_AGENT;
|
|
99
|
+
this.autoPaceWhatsapp = options.autoPaceWhatsapp !== false;
|
|
80
100
|
}
|
|
81
101
|
/** Returns true when using a znd_test_ key (sandbox / no real delivery). */
|
|
82
102
|
isTestMode() {
|
|
83
103
|
return this.apiKey.startsWith("znd_test_");
|
|
84
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Bind this API key to your site (WordPress connect flow).
|
|
107
|
+
* First call wins — the same key cannot be used on another site URL.
|
|
108
|
+
*/
|
|
109
|
+
async connect(siteUrl) {
|
|
110
|
+
const url = (0, validate_1.validateSiteUrl)(siteUrl) ?? this.siteUrl;
|
|
111
|
+
const data = await this.request("POST", "connect", {
|
|
112
|
+
siteUrl: url,
|
|
113
|
+
});
|
|
114
|
+
if (data.ok !== true || !data.project || !Array.isArray(data.templates)) {
|
|
115
|
+
throw new errors_1.ZinduaError("API response missing connect payload.", {
|
|
116
|
+
status: 200,
|
|
117
|
+
code: "INVALID_RESPONSE",
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
ok: true,
|
|
122
|
+
connected: true,
|
|
123
|
+
siteUrl: typeof data.siteUrl === "string" ? data.siteUrl : url ?? "",
|
|
124
|
+
newlyBound: Boolean(data.newlyBound),
|
|
125
|
+
project: data.project,
|
|
126
|
+
templates: data.templates,
|
|
127
|
+
plan: data.plan ?? null,
|
|
128
|
+
channels: data.channels ?? {
|
|
129
|
+
email: { ready: false },
|
|
130
|
+
whatsapp: { ready: false },
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** Project status, channels, and plan (GET /project). */
|
|
135
|
+
async getProject() {
|
|
136
|
+
return this.request("GET", "project");
|
|
137
|
+
}
|
|
138
|
+
/** Synced templates with langs and variables (GET /templates). */
|
|
139
|
+
async getTemplates() {
|
|
140
|
+
const data = await this.request("GET", "templates");
|
|
141
|
+
return {
|
|
142
|
+
templates: Array.isArray(data.templates) ? data.templates : [],
|
|
143
|
+
limits: data.limits,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/** Delivery status for a logId returned by send() (GET /logs/{logId}). */
|
|
147
|
+
async getLog(logId) {
|
|
148
|
+
const trimmed = logId.trim();
|
|
149
|
+
if (!trimmed) {
|
|
150
|
+
throw new errors_1.ZinduaError("logId is required.", { status: 0, code: "MISSING_FIELDS" });
|
|
151
|
+
}
|
|
152
|
+
const data = await this.request("GET", `logs/${encodeURIComponent(trimmed)}`);
|
|
153
|
+
if (data.ok !== true || !data.log || typeof data.log !== "object") {
|
|
154
|
+
throw new errors_1.ZinduaError("API response missing log payload.", {
|
|
155
|
+
status: 200,
|
|
156
|
+
code: "INVALID_RESPONSE",
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return data.log;
|
|
160
|
+
}
|
|
85
161
|
async send(options) {
|
|
86
162
|
const channel = (0, validate_1.validateChannel)(options.channel);
|
|
163
|
+
if (channel === "whatsapp" && this.autoPaceWhatsapp) {
|
|
164
|
+
await this.paceWhatsappSend();
|
|
165
|
+
}
|
|
87
166
|
const payload = buildPayload(options, channel);
|
|
167
|
+
const data = await this.request("POST", "send", payload);
|
|
168
|
+
if (data.success !== true || typeof data.logId !== "string") {
|
|
169
|
+
throw new errors_1.ZinduaError("API response missing success or logId.", {
|
|
170
|
+
status: 200,
|
|
171
|
+
code: "INVALID_RESPONSE",
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
if (channel === "whatsapp") {
|
|
175
|
+
this.lastWhatsappSendAt = Date.now();
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
success: true,
|
|
179
|
+
channel: (data.channel === "whatsapp" ? "whatsapp" : "email"),
|
|
180
|
+
status: typeof data.status === "string" ? data.status : "queued",
|
|
181
|
+
logId: data.logId,
|
|
182
|
+
langUsed: typeof data.langUsed === "string" ? data.langUsed : undefined,
|
|
183
|
+
langFallback: typeof data.langFallback === "boolean" ? data.langFallback : undefined,
|
|
184
|
+
testMode: typeof data.testMode === "boolean" ? data.testMode : undefined,
|
|
185
|
+
project: typeof data.project === "string" ? data.project : undefined,
|
|
186
|
+
context: data.context && typeof data.context === "object"
|
|
187
|
+
? data.context
|
|
188
|
+
: undefined,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/** Serialize WhatsApp sends on this client and wait for the anti-ban gap. */
|
|
192
|
+
paceWhatsappSend() {
|
|
193
|
+
const run = async () => {
|
|
194
|
+
const elapsed = Date.now() - this.lastWhatsappSendAt;
|
|
195
|
+
if (this.lastWhatsappSendAt > 0 && elapsed < whatsapp_anti_ban_1.WHATSAPP_MIN_INTERVAL_MS) {
|
|
196
|
+
const waitMs = whatsapp_anti_ban_1.WHATSAPP_MIN_INTERVAL_MS - elapsed;
|
|
197
|
+
(0, whatsapp_anti_ban_1.warnWhatsappAntiBanPace)(waitMs);
|
|
198
|
+
await (0, whatsapp_anti_ban_1.sleepMs)(waitMs);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
this.whatsappPaceChain = this.whatsappPaceChain.then(run, run);
|
|
202
|
+
return this.whatsappPaceChain;
|
|
203
|
+
}
|
|
204
|
+
buildHeaders() {
|
|
205
|
+
const headers = {
|
|
206
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
207
|
+
"User-Agent": this.userAgent,
|
|
208
|
+
Accept: "application/json",
|
|
209
|
+
};
|
|
210
|
+
if (this.siteUrl) {
|
|
211
|
+
headers["X-Zindua-Site-Url"] = this.siteUrl;
|
|
212
|
+
}
|
|
213
|
+
return headers;
|
|
214
|
+
}
|
|
215
|
+
async request(method, path, body) {
|
|
216
|
+
const url = `${this.apiBase}/${path.replace(/^\//, "")}`;
|
|
88
217
|
const controller = new AbortController();
|
|
89
218
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
90
219
|
try {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
headers
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
body: JSON.stringify(payload),
|
|
220
|
+
const headers = this.buildHeaders();
|
|
221
|
+
if (body) {
|
|
222
|
+
headers["Content-Type"] = "application/json";
|
|
223
|
+
}
|
|
224
|
+
const response = await fetch(url, {
|
|
225
|
+
method,
|
|
226
|
+
headers,
|
|
227
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
100
228
|
signal: controller.signal,
|
|
101
229
|
redirect: "error",
|
|
102
230
|
});
|
|
@@ -114,25 +242,7 @@ class Zindua {
|
|
|
114
242
|
if (!response.ok) {
|
|
115
243
|
throw parseApiError(response.status, data);
|
|
116
244
|
}
|
|
117
|
-
|
|
118
|
-
throw new errors_1.ZinduaError("API response missing success or logId.", {
|
|
119
|
-
status: response.status,
|
|
120
|
-
code: "INVALID_RESPONSE",
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
return {
|
|
124
|
-
success: true,
|
|
125
|
-
channel: (data.channel === "whatsapp" ? "whatsapp" : "email"),
|
|
126
|
-
status: typeof data.status === "string" ? data.status : "queued",
|
|
127
|
-
logId: data.logId,
|
|
128
|
-
langUsed: typeof data.langUsed === "string" ? data.langUsed : undefined,
|
|
129
|
-
langFallback: typeof data.langFallback === "boolean" ? data.langFallback : undefined,
|
|
130
|
-
testMode: typeof data.testMode === "boolean" ? data.testMode : undefined,
|
|
131
|
-
project: typeof data.project === "string" ? data.project : undefined,
|
|
132
|
-
context: data.context && typeof data.context === "object"
|
|
133
|
-
? data.context
|
|
134
|
-
: undefined,
|
|
135
|
-
};
|
|
245
|
+
return data;
|
|
136
246
|
}
|
|
137
247
|
catch (err) {
|
|
138
248
|
if (err instanceof errors_1.ZinduaError)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { Zindua } from "./client";
|
|
2
|
-
export type { ZinduaClientOptions, ZinduaSendContext, ZinduaSendOptions, ZinduaSendResult, SendChannel, } from "./client";
|
|
2
|
+
export type { ZinduaClientOptions, ZinduaConnectResult, ZinduaProjectInfo, ZinduaSendContext, ZinduaSendOptions, ZinduaSendResult, ZinduaTemplateInfo, SendChannel, } from "./client";
|
|
3
3
|
export { ZinduaError } from "./errors";
|
|
4
4
|
export type { ZinduaErrorCode } from "./errors";
|
|
5
5
|
export { DEFAULT_API_BASE, LIMITS } from "./validate";
|
|
6
|
+
export { WHATSAPP_ANTI_BAN_GUIDE_URL, WHATSAPP_MIN_INTERVAL_MS, } from "./whatsapp-anti-ban";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.LIMITS = exports.DEFAULT_API_BASE = exports.ZinduaError = exports.Zindua = void 0;
|
|
3
|
+
exports.WHATSAPP_MIN_INTERVAL_MS = exports.WHATSAPP_ANTI_BAN_GUIDE_URL = exports.LIMITS = exports.DEFAULT_API_BASE = exports.ZinduaError = exports.Zindua = void 0;
|
|
4
4
|
var client_1 = require("./client");
|
|
5
5
|
Object.defineProperty(exports, "Zindua", { enumerable: true, get: function () { return client_1.Zindua; } });
|
|
6
6
|
var errors_1 = require("./errors");
|
|
@@ -8,3 +8,6 @@ Object.defineProperty(exports, "ZinduaError", { enumerable: true, get: function
|
|
|
8
8
|
var validate_1 = require("./validate");
|
|
9
9
|
Object.defineProperty(exports, "DEFAULT_API_BASE", { enumerable: true, get: function () { return validate_1.DEFAULT_API_BASE; } });
|
|
10
10
|
Object.defineProperty(exports, "LIMITS", { enumerable: true, get: function () { return validate_1.LIMITS; } });
|
|
11
|
+
var whatsapp_anti_ban_1 = require("./whatsapp-anti-ban");
|
|
12
|
+
Object.defineProperty(exports, "WHATSAPP_ANTI_BAN_GUIDE_URL", { enumerable: true, get: function () { return whatsapp_anti_ban_1.WHATSAPP_ANTI_BAN_GUIDE_URL; } });
|
|
13
|
+
Object.defineProperty(exports, "WHATSAPP_MIN_INTERVAL_MS", { enumerable: true, get: function () { return whatsapp_anti_ban_1.WHATSAPP_MIN_INTERVAL_MS; } });
|
package/dist/validate.d.ts
CHANGED
|
@@ -23,3 +23,5 @@ export declare function sanitizeVariables(variables?: Record<string, string>): R
|
|
|
23
23
|
export declare function validateOptionalEmailField(field: "cc" | "bcc" | "replyTo", value?: string): string | undefined;
|
|
24
24
|
export declare function validateAttachments(attachments?: unknown[]): unknown[] | undefined;
|
|
25
25
|
export declare function validateTimeoutMs(timeoutMs?: number): number;
|
|
26
|
+
/** Normalize site URL for WordPress one-key-one-site binding (protocol + host). */
|
|
27
|
+
export declare function validateSiteUrl(siteUrl?: string): string | undefined;
|
package/dist/validate.js
CHANGED
|
@@ -13,6 +13,7 @@ exports.sanitizeVariables = sanitizeVariables;
|
|
|
13
13
|
exports.validateOptionalEmailField = validateOptionalEmailField;
|
|
14
14
|
exports.validateAttachments = validateAttachments;
|
|
15
15
|
exports.validateTimeoutMs = validateTimeoutMs;
|
|
16
|
+
exports.validateSiteUrl = validateSiteUrl;
|
|
16
17
|
const errors_1 = require("./errors");
|
|
17
18
|
/** Matches platform `isValidE164Phone` in zindua.run API. */
|
|
18
19
|
const E164_RE = /^\+[1-9]\d{6,14}$/;
|
|
@@ -214,3 +215,21 @@ function validateTimeoutMs(timeoutMs) {
|
|
|
214
215
|
}
|
|
215
216
|
return Math.floor(timeoutMs);
|
|
216
217
|
}
|
|
218
|
+
/** Normalize site URL for WordPress one-key-one-site binding (protocol + host). */
|
|
219
|
+
function validateSiteUrl(siteUrl) {
|
|
220
|
+
if (siteUrl === undefined || siteUrl === null)
|
|
221
|
+
return undefined;
|
|
222
|
+
const trimmed = String(siteUrl).trim();
|
|
223
|
+
if (!trimmed)
|
|
224
|
+
return undefined;
|
|
225
|
+
try {
|
|
226
|
+
const url = new URL(trimmed.includes("://") ? trimmed : `https://${trimmed}`);
|
|
227
|
+
return `${url.protocol}//${url.host}`;
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
throw new errors_1.ZinduaError("siteUrl must be a valid URL (e.g. https://example.com).", {
|
|
231
|
+
status: 0,
|
|
232
|
+
code: "INVALID_OPTIONS",
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Zindua WhatsApp anti-ban robot — client-side pacing helpers. */
|
|
2
|
+
export declare const WHATSAPP_MIN_INTERVAL_MS = 3000;
|
|
3
|
+
/** Guide for developers integrating WhatsApp OTP safely. */
|
|
4
|
+
export declare const WHATSAPP_ANTI_BAN_GUIDE_URL = "https://zindua.run/blog/whatsapp-otp-anti-ban-pacing";
|
|
5
|
+
export declare function antiBanRateLimitMessage(baseMessage: string, retryAfterSec?: number): string;
|
|
6
|
+
export declare function warnWhatsappAntiBanPace(waitMs: number): void;
|
|
7
|
+
export declare function sleepMs(ms: number): Promise<void>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/** Zindua WhatsApp anti-ban robot — client-side pacing helpers. */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.WHATSAPP_ANTI_BAN_GUIDE_URL = exports.WHATSAPP_MIN_INTERVAL_MS = void 0;
|
|
5
|
+
exports.antiBanRateLimitMessage = antiBanRateLimitMessage;
|
|
6
|
+
exports.warnWhatsappAntiBanPace = warnWhatsappAntiBanPace;
|
|
7
|
+
exports.sleepMs = sleepMs;
|
|
8
|
+
exports.WHATSAPP_MIN_INTERVAL_MS = 3000;
|
|
9
|
+
/** Guide for developers integrating WhatsApp OTP safely. */
|
|
10
|
+
exports.WHATSAPP_ANTI_BAN_GUIDE_URL = "https://zindua.run/blog/whatsapp-otp-anti-ban-pacing";
|
|
11
|
+
function antiBanRateLimitMessage(baseMessage, retryAfterSec) {
|
|
12
|
+
const wait = typeof retryAfterSec === "number" && retryAfterSec > 0
|
|
13
|
+
? ` Wait ${retryAfterSec}s, then retry.`
|
|
14
|
+
: " Wait at least 3s between WhatsApp sends.";
|
|
15
|
+
return (`${baseMessage}${wait} ` +
|
|
16
|
+
`Zindua's anti-ban robot paces WhatsApp to protect your linked number from bans. ` +
|
|
17
|
+
`Space sends in your app (queue / debounce) or keep SDK autoPaceWhatsapp on. ` +
|
|
18
|
+
`Guide: ${exports.WHATSAPP_ANTI_BAN_GUIDE_URL}`);
|
|
19
|
+
}
|
|
20
|
+
function warnWhatsappAntiBanPace(waitMs) {
|
|
21
|
+
const sec = Math.ceil(waitMs / 1000);
|
|
22
|
+
console.warn(`[Zindua anti-ban] Waiting ${sec}s before WhatsApp send — ` +
|
|
23
|
+
`rapid OTP looks like spam and can ban your linked number. ` +
|
|
24
|
+
`Prefer a queue or ≥3s gap in your code. ${exports.WHATSAPP_ANTI_BAN_GUIDE_URL}`);
|
|
25
|
+
}
|
|
26
|
+
async function sleepMs(ms) {
|
|
27
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
28
|
+
}
|