@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 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 sendUrl;
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";
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
- sendUrl;
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
- const base = (0, validate_1.resolveBaseUrl)(options.baseUrl);
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 response = await fetch(this.sendUrl, {
92
- method: "POST",
93
- headers: {
94
- "Content-Type": "application/json",
95
- Authorization: `Bearer ${this.apiKey}`,
96
- "User-Agent": USER_AGENT,
97
- Accept: "application/json",
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
- if (data.success !== true || typeof data.logId !== "string") {
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";
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zindua/sdk",
3
- "version": "1.2.6",
3
+ "version": "1.2.7",
4
4
  "description": "Official Zindua SDK for Node.js — transactional email and WhatsApp via POST /api/v1/send.",
5
5
  "author": "Zindua",
6
6
  "license": "MIT",