@zindua/sdk 1.2.6 → 1.2.7
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 +28 -0
- package/dist/client.d.ts +51 -1
- package/dist/client.js +91 -32
- package/dist/index.d.ts +1 -1
- package/dist/validate.d.ts +2 -0
- package/dist/validate.js +19 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -369,6 +369,32 @@ Do **not** embed `@zindua/sdk` or `znd_live_…` in the app. Call your backend r
|
|
|
369
369
|
|
|
370
370
|
---
|
|
371
371
|
|
|
372
|
+
## WordPress site binding & `connect()`
|
|
373
|
+
|
|
374
|
+
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`:
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
const zindua = new Zindua({
|
|
378
|
+
apiKey: process.env.ZINDUA_API_KEY!,
|
|
379
|
+
siteUrl: "https://shop.example.com",
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// First connect binds the key to this URL
|
|
383
|
+
const status = await zindua.connect();
|
|
384
|
+
console.log(status.project.name, status.templates);
|
|
385
|
+
|
|
386
|
+
// Later: list templates with langs (fr, en, …)
|
|
387
|
+
const { templates } = await zindua.getTemplates();
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
| Error code | Meaning |
|
|
391
|
+
|------------|---------|
|
|
392
|
+
| `SITE_ALREADY_BOUND` | Key already used on another site — create a new project or regenerate the key |
|
|
393
|
+
| `SITE_MISMATCH` | `siteUrl` does not match the registered site |
|
|
394
|
+
| `SITE_URL_REQUIRED` | Key is bound; update the SDK and set `siteUrl` |
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
372
398
|
## Configuration options
|
|
373
399
|
|
|
374
400
|
| Option | Default | Description |
|
|
@@ -376,10 +402,12 @@ Do **not** embed `@zindua/sdk` or `znd_live_…` in the app. Call your backend r
|
|
|
376
402
|
| `apiKey` | — | **Required.** `znd_live_…` or `znd_test_…` from the dashboard. |
|
|
377
403
|
| `baseUrl` | `https://zindua.run/api/v1` | Override only for local testing (`http://localhost:3000/api/v1`). |
|
|
378
404
|
| `timeoutMs` | `30000` | Request timeout (max `120000`). |
|
|
405
|
+
| `siteUrl` | — | Your app origin (`https://example.com`). Required once the key is site-bound. |
|
|
379
406
|
|
|
380
407
|
```typescript
|
|
381
408
|
const zindua = new Zindua({
|
|
382
409
|
apiKey: process.env.ZINDUA_API_KEY!,
|
|
410
|
+
siteUrl: process.env.APP_URL,
|
|
383
411
|
baseUrl: process.env.ZINDUA_API_BASE_URL, // optional
|
|
384
412
|
timeoutMs: 45_000,
|
|
385
413
|
});
|
package/dist/client.d.ts
CHANGED
|
@@ -16,6 +16,41 @@ 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
|
+
export type ZinduaProjectInfo = {
|
|
26
|
+
name: string;
|
|
27
|
+
slug: string;
|
|
28
|
+
defaultLang: string;
|
|
29
|
+
dashboardUrl: string;
|
|
30
|
+
};
|
|
31
|
+
export type ZinduaTemplateInfo = {
|
|
32
|
+
slug: string;
|
|
33
|
+
langs: string[];
|
|
34
|
+
defaultLang: string;
|
|
35
|
+
variables: string[];
|
|
36
|
+
};
|
|
37
|
+
export type ZinduaConnectResult = {
|
|
38
|
+
ok: true;
|
|
39
|
+
connected: true;
|
|
40
|
+
siteUrl: string;
|
|
41
|
+
newlyBound: boolean;
|
|
42
|
+
project: ZinduaProjectInfo;
|
|
43
|
+
templates: ZinduaTemplateInfo[];
|
|
44
|
+
plan: ZinduaSendContext["plan"];
|
|
45
|
+
channels: {
|
|
46
|
+
email: {
|
|
47
|
+
ready: boolean;
|
|
48
|
+
};
|
|
49
|
+
whatsapp: {
|
|
50
|
+
ready: boolean;
|
|
51
|
+
status?: string;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
19
54
|
};
|
|
20
55
|
export type ZinduaSendContext = {
|
|
21
56
|
project: {
|
|
@@ -58,10 +93,25 @@ export type ZinduaSendResult = {
|
|
|
58
93
|
};
|
|
59
94
|
export declare class Zindua {
|
|
60
95
|
private readonly apiKey;
|
|
61
|
-
private readonly
|
|
96
|
+
private readonly apiBase;
|
|
62
97
|
private readonly timeoutMs;
|
|
98
|
+
private readonly siteUrl?;
|
|
63
99
|
constructor(options: ZinduaClientOptions);
|
|
64
100
|
/** Returns true when using a znd_test_ key (sandbox / no real delivery). */
|
|
65
101
|
isTestMode(): boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Bind this API key to your site (WordPress connect flow).
|
|
104
|
+
* First call wins — the same key cannot be used on another site URL.
|
|
105
|
+
*/
|
|
106
|
+
connect(siteUrl?: string): Promise<ZinduaConnectResult>;
|
|
107
|
+
/** Project status, channels, and plan (GET /project). */
|
|
108
|
+
getProject(): Promise<Record<string, unknown>>;
|
|
109
|
+
/** Synced templates with langs and variables (GET /templates). */
|
|
110
|
+
getTemplates(): Promise<{
|
|
111
|
+
templates: ZinduaTemplateInfo[];
|
|
112
|
+
limits?: Record<string, unknown>;
|
|
113
|
+
}>;
|
|
66
114
|
send(options: ZinduaSendOptions): Promise<ZinduaSendResult>;
|
|
115
|
+
private buildHeaders;
|
|
116
|
+
private request;
|
|
67
117
|
}
|
package/dist/client.js
CHANGED
|
@@ -3,7 +3,7 @@ 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 SDK_VERSION = "1.2.
|
|
6
|
+
const SDK_VERSION = "1.2.7";
|
|
7
7
|
const USER_AGENT = `Zindua-JS/${SDK_VERSION}`;
|
|
8
8
|
function buildPayload(options, channel) {
|
|
9
9
|
const to = (0, validate_1.validateRecipient)(options.to, channel);
|
|
@@ -50,6 +50,8 @@ function parseApiError(status, body) {
|
|
|
50
50
|
details.hint = body.hint;
|
|
51
51
|
if (typeof body.retryAfterSec === "number")
|
|
52
52
|
details.retryAfterSec = body.retryAfterSec;
|
|
53
|
+
if (typeof body.boundSite === "string")
|
|
54
|
+
details.boundSite = body.boundSite;
|
|
53
55
|
if (Array.isArray(body.availableTemplateSlugs)) {
|
|
54
56
|
details.availableTemplateSlugs = body.availableTemplateSlugs;
|
|
55
57
|
}
|
|
@@ -69,34 +71,109 @@ function parseApiError(status, body) {
|
|
|
69
71
|
}
|
|
70
72
|
class Zindua {
|
|
71
73
|
apiKey;
|
|
72
|
-
|
|
74
|
+
apiBase;
|
|
73
75
|
timeoutMs;
|
|
76
|
+
siteUrl;
|
|
74
77
|
constructor(options) {
|
|
75
78
|
(0, validate_1.assertServerRuntime)();
|
|
76
79
|
this.apiKey = (0, validate_1.validateApiKey)(options.apiKey);
|
|
77
|
-
|
|
78
|
-
this.sendUrl = `${base}/send`;
|
|
80
|
+
this.apiBase = (0, validate_1.resolveBaseUrl)(options.baseUrl);
|
|
79
81
|
this.timeoutMs = (0, validate_1.validateTimeoutMs)(options.timeoutMs);
|
|
82
|
+
this.siteUrl = (0, validate_1.validateSiteUrl)(options.siteUrl);
|
|
80
83
|
}
|
|
81
84
|
/** Returns true when using a znd_test_ key (sandbox / no real delivery). */
|
|
82
85
|
isTestMode() {
|
|
83
86
|
return this.apiKey.startsWith("znd_test_");
|
|
84
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Bind this API key to your site (WordPress connect flow).
|
|
90
|
+
* First call wins — the same key cannot be used on another site URL.
|
|
91
|
+
*/
|
|
92
|
+
async connect(siteUrl) {
|
|
93
|
+
const url = (0, validate_1.validateSiteUrl)(siteUrl) ?? this.siteUrl;
|
|
94
|
+
const data = await this.request("POST", "connect", {
|
|
95
|
+
siteUrl: url,
|
|
96
|
+
});
|
|
97
|
+
if (data.ok !== true || !data.project || !Array.isArray(data.templates)) {
|
|
98
|
+
throw new errors_1.ZinduaError("API response missing connect payload.", {
|
|
99
|
+
status: 200,
|
|
100
|
+
code: "INVALID_RESPONSE",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
ok: true,
|
|
105
|
+
connected: true,
|
|
106
|
+
siteUrl: typeof data.siteUrl === "string" ? data.siteUrl : url ?? "",
|
|
107
|
+
newlyBound: Boolean(data.newlyBound),
|
|
108
|
+
project: data.project,
|
|
109
|
+
templates: data.templates,
|
|
110
|
+
plan: data.plan ?? null,
|
|
111
|
+
channels: data.channels ?? {
|
|
112
|
+
email: { ready: false },
|
|
113
|
+
whatsapp: { ready: false },
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** Project status, channels, and plan (GET /project). */
|
|
118
|
+
async getProject() {
|
|
119
|
+
return this.request("GET", "project");
|
|
120
|
+
}
|
|
121
|
+
/** Synced templates with langs and variables (GET /templates). */
|
|
122
|
+
async getTemplates() {
|
|
123
|
+
const data = await this.request("GET", "templates");
|
|
124
|
+
return {
|
|
125
|
+
templates: Array.isArray(data.templates) ? data.templates : [],
|
|
126
|
+
limits: data.limits,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
85
129
|
async send(options) {
|
|
86
130
|
const channel = (0, validate_1.validateChannel)(options.channel);
|
|
87
131
|
const payload = buildPayload(options, channel);
|
|
132
|
+
const data = await this.request("POST", "send", payload);
|
|
133
|
+
if (data.success !== true || typeof data.logId !== "string") {
|
|
134
|
+
throw new errors_1.ZinduaError("API response missing success or logId.", {
|
|
135
|
+
status: 200,
|
|
136
|
+
code: "INVALID_RESPONSE",
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
success: true,
|
|
141
|
+
channel: (data.channel === "whatsapp" ? "whatsapp" : "email"),
|
|
142
|
+
status: typeof data.status === "string" ? data.status : "queued",
|
|
143
|
+
logId: data.logId,
|
|
144
|
+
langUsed: typeof data.langUsed === "string" ? data.langUsed : undefined,
|
|
145
|
+
langFallback: typeof data.langFallback === "boolean" ? data.langFallback : undefined,
|
|
146
|
+
testMode: typeof data.testMode === "boolean" ? data.testMode : undefined,
|
|
147
|
+
project: typeof data.project === "string" ? data.project : undefined,
|
|
148
|
+
context: data.context && typeof data.context === "object"
|
|
149
|
+
? data.context
|
|
150
|
+
: undefined,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
buildHeaders() {
|
|
154
|
+
const headers = {
|
|
155
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
156
|
+
"User-Agent": USER_AGENT,
|
|
157
|
+
Accept: "application/json",
|
|
158
|
+
};
|
|
159
|
+
if (this.siteUrl) {
|
|
160
|
+
headers["X-Zindua-Site-Url"] = this.siteUrl;
|
|
161
|
+
}
|
|
162
|
+
return headers;
|
|
163
|
+
}
|
|
164
|
+
async request(method, path, body) {
|
|
165
|
+
const url = `${this.apiBase}/${path.replace(/^\//, "")}`;
|
|
88
166
|
const controller = new AbortController();
|
|
89
167
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
90
168
|
try {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
headers
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
body: JSON.stringify(payload),
|
|
169
|
+
const headers = this.buildHeaders();
|
|
170
|
+
if (body) {
|
|
171
|
+
headers["Content-Type"] = "application/json";
|
|
172
|
+
}
|
|
173
|
+
const response = await fetch(url, {
|
|
174
|
+
method,
|
|
175
|
+
headers,
|
|
176
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
100
177
|
signal: controller.signal,
|
|
101
178
|
redirect: "error",
|
|
102
179
|
});
|
|
@@ -114,25 +191,7 @@ class Zindua {
|
|
|
114
191
|
if (!response.ok) {
|
|
115
192
|
throw parseApiError(response.status, data);
|
|
116
193
|
}
|
|
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
|
-
};
|
|
194
|
+
return data;
|
|
136
195
|
}
|
|
137
196
|
catch (err) {
|
|
138
197
|
if (err instanceof errors_1.ZinduaError)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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";
|
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
|
+
}
|