@zindua/sdk 1.2.7 → 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 +19 -1
- package/dist/client.d.ts +19 -0
- package/dist/client.js +56 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -1
- 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.
|
|
@@ -534,7 +552,7 @@ try {
|
|
|
534
552
|
| `WHATSAPP_NOT_CONNECTED` | 422 | WhatsApp not linked. | Dashboard → project → **WhatsApp** → scan QR. |
|
|
535
553
|
| `WHATSAPP_PAUSED` | 422 | WhatsApp session paused. | Resume in the dashboard. |
|
|
536
554
|
| `WHATSAPP_QUOTA_EXCEEDED` | 429 | Monthly WhatsApp limit reached. | Upgrade or wait. |
|
|
537
|
-
| `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). |
|
|
538
556
|
| `TEMPLATE_NOT_FOUND` | 404 | Unknown template slug. | Create template or fix slug; check `availableTemplateSlugs` in details. |
|
|
539
557
|
| `TEMPLATE_NO_CONTENT` | 404 | Template has no language version. | Add content in Dashboard → Templates. |
|
|
540
558
|
| `TEMPLATE_NO_WHATSAPP_BODY` | 422 | No plain text for WhatsApp. | Add a text body for that language. |
|
package/dist/client.d.ts
CHANGED
|
@@ -21,6 +21,17 @@ export type ZinduaClientOptions = {
|
|
|
21
21
|
* Sent as X-Zindua-Site-Url — required once the API key is bound to a site.
|
|
22
22
|
*/
|
|
23
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;
|
|
24
35
|
};
|
|
25
36
|
export type ZinduaProjectInfo = {
|
|
26
37
|
name: string;
|
|
@@ -96,6 +107,10 @@ export declare class Zindua {
|
|
|
96
107
|
private readonly apiBase;
|
|
97
108
|
private readonly timeoutMs;
|
|
98
109
|
private readonly siteUrl?;
|
|
110
|
+
private readonly userAgent;
|
|
111
|
+
private readonly autoPaceWhatsapp;
|
|
112
|
+
private lastWhatsappSendAt;
|
|
113
|
+
private whatsappPaceChain;
|
|
99
114
|
constructor(options: ZinduaClientOptions);
|
|
100
115
|
/** Returns true when using a znd_test_ key (sandbox / no real delivery). */
|
|
101
116
|
isTestMode(): boolean;
|
|
@@ -111,7 +126,11 @@ export declare class Zindua {
|
|
|
111
126
|
templates: ZinduaTemplateInfo[];
|
|
112
127
|
limits?: Record<string, unknown>;
|
|
113
128
|
}>;
|
|
129
|
+
/** Delivery status for a logId returned by send() (GET /logs/{logId}). */
|
|
130
|
+
getLog(logId: string): Promise<Record<string, unknown>>;
|
|
114
131
|
send(options: ZinduaSendOptions): Promise<ZinduaSendResult>;
|
|
132
|
+
/** Serialize WhatsApp sends on this client and wait for the anti-ban gap. */
|
|
133
|
+
private paceWhatsappSend;
|
|
115
134
|
private buildHeaders;
|
|
116
135
|
private request;
|
|
117
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
|
|
@@ -58,10 +59,17 @@ function parseApiError(status, body) {
|
|
|
58
59
|
if (body.context && typeof body.context === "object") {
|
|
59
60
|
details.context = body.context;
|
|
60
61
|
}
|
|
61
|
-
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;
|
|
62
70
|
return new errors_1.ZinduaError(fullMessage, {
|
|
63
71
|
status,
|
|
64
|
-
code
|
|
72
|
+
code,
|
|
65
73
|
details: Object.keys(details).length > 0
|
|
66
74
|
? details
|
|
67
75
|
: typeof body.hint === "string"
|
|
@@ -74,12 +82,21 @@ class Zindua {
|
|
|
74
82
|
apiBase;
|
|
75
83
|
timeoutMs;
|
|
76
84
|
siteUrl;
|
|
85
|
+
userAgent;
|
|
86
|
+
autoPaceWhatsapp;
|
|
87
|
+
lastWhatsappSendAt = 0;
|
|
88
|
+
whatsappPaceChain = Promise.resolve();
|
|
77
89
|
constructor(options) {
|
|
78
90
|
(0, validate_1.assertServerRuntime)();
|
|
79
91
|
this.apiKey = (0, validate_1.validateApiKey)(options.apiKey);
|
|
80
92
|
this.apiBase = (0, validate_1.resolveBaseUrl)(options.baseUrl);
|
|
81
93
|
this.timeoutMs = (0, validate_1.validateTimeoutMs)(options.timeoutMs);
|
|
82
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;
|
|
83
100
|
}
|
|
84
101
|
/** Returns true when using a znd_test_ key (sandbox / no real delivery). */
|
|
85
102
|
isTestMode() {
|
|
@@ -126,8 +143,26 @@ class Zindua {
|
|
|
126
143
|
limits: data.limits,
|
|
127
144
|
};
|
|
128
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
|
+
}
|
|
129
161
|
async send(options) {
|
|
130
162
|
const channel = (0, validate_1.validateChannel)(options.channel);
|
|
163
|
+
if (channel === "whatsapp" && this.autoPaceWhatsapp) {
|
|
164
|
+
await this.paceWhatsappSend();
|
|
165
|
+
}
|
|
131
166
|
const payload = buildPayload(options, channel);
|
|
132
167
|
const data = await this.request("POST", "send", payload);
|
|
133
168
|
if (data.success !== true || typeof data.logId !== "string") {
|
|
@@ -136,6 +171,9 @@ class Zindua {
|
|
|
136
171
|
code: "INVALID_RESPONSE",
|
|
137
172
|
});
|
|
138
173
|
}
|
|
174
|
+
if (channel === "whatsapp") {
|
|
175
|
+
this.lastWhatsappSendAt = Date.now();
|
|
176
|
+
}
|
|
139
177
|
return {
|
|
140
178
|
success: true,
|
|
141
179
|
channel: (data.channel === "whatsapp" ? "whatsapp" : "email"),
|
|
@@ -150,10 +188,23 @@ class Zindua {
|
|
|
150
188
|
: undefined,
|
|
151
189
|
};
|
|
152
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
|
+
}
|
|
153
204
|
buildHeaders() {
|
|
154
205
|
const headers = {
|
|
155
206
|
Authorization: `Bearer ${this.apiKey}`,
|
|
156
|
-
"User-Agent":
|
|
207
|
+
"User-Agent": this.userAgent,
|
|
157
208
|
Accept: "application/json",
|
|
158
209
|
};
|
|
159
210
|
if (this.siteUrl) {
|
package/dist/index.d.ts
CHANGED
|
@@ -3,3 +3,4 @@ export type { ZinduaClientOptions, ZinduaConnectResult, ZinduaProjectInfo, Zindu
|
|
|
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; } });
|
|
@@ -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
|
+
}
|