@ayush0x44/notifystack 1.0.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/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # NotifyStack Node.js SDK
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@ayush0x44/notifystack.svg)](https://www.npmjs.com/package/@ayush0x44/notifystack)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ The official Node.js SDK for **NotifyStack** — a high-performance, distributed notification SaaS platform. Send Emails, SMS, Push, and In-App notifications with a single unified API.
7
+
8
+ ## 🚀 Features
9
+
10
+ - **Zero Dependencies**: Lightweight and fast (uses native `fetch`).
11
+ - **Unified API**: One interface for all channels (Email, SMS, Push, In-App).
12
+ - **Auto-Retry**: Built-in exponential backoff for network flakes.
13
+ - **Idempotency**: Safe retries without duplicate notifications.
14
+ - **Batching**: Send up to 100 notifications in a single call.
15
+
16
+ ## 📦 Installation
17
+
18
+ ```bash
19
+ npm install @ayush0x44/notifystack
20
+ ```
21
+
22
+ ## 🛠️ Quick Start
23
+
24
+ ```javascript
25
+ const { NotifySDK } = require("@ayush0x44/notifystack");
26
+
27
+ // Initialize the client
28
+ const notify = new NotifySDK("ntf_live_your_api_key", {
29
+ baseUrl: "https://notificationsaas.onrender.com" // Point to your cloud API
30
+ });
31
+
32
+ async function main() {
33
+ // 1. Send an Event-based notification (uses templates)
34
+ await notify.track("ORDER_PLACED", {
35
+ email: "customer@example.com",
36
+ orderId: "ORD-123"
37
+ });
38
+
39
+ // 2. Send a direct Email
40
+ await notify.send({
41
+ to: "hello@world.com",
42
+ subject: "Welcome!",
43
+ body: "Thanks for joining our platform."
44
+ });
45
+
46
+ // 3. Send an SMS
47
+ await notify.sendSms({
48
+ to: "+1234567890",
49
+ body: "Your verification code is 1234"
50
+ });
51
+ }
52
+
53
+ main().catch(console.error);
54
+ ```
55
+
56
+ ## ⚙️ Configuration
57
+
58
+ | Option | Type | Default | Description |
59
+ | :--- | :--- | :--- | :--- |
60
+ | `baseUrl` | `string` | `http://localhost:3000` | The URL of your NotifyStack API |
61
+ | `maxRetries` | `number` | `3` | Max attempts for failed requests |
62
+ | `timeoutMs` | `number` | `10000` | Request timeout duration |
63
+ | `debug` | `boolean` | `false` | Enable verbose logging |
64
+
65
+ ## 📖 License
66
+
67
+ MIT © [Ayush](https://github.com/ayush462)
package/index.d.ts ADDED
@@ -0,0 +1,67 @@
1
+ declare class NotifySDK {
2
+ constructor(apiKey: string, options?: {
3
+ baseUrl?: string;
4
+ maxRetries?: number;
5
+ timeoutMs?: number;
6
+ debug?: boolean;
7
+ });
8
+
9
+ /** Send event-based notification using a template */
10
+ track(eventName: string, data: Record<string, any> & { email: string }, options?: {
11
+ metadata?: Record<string, any>;
12
+ channel?: "email" | "sms" | "push";
13
+ priority?: "low" | "normal" | "high";
14
+ }): Promise<{ id: string; status: string }>;
15
+
16
+ /** Send a direct email */
17
+ send(params: {
18
+ to: string;
19
+ subject: string;
20
+ body: string;
21
+ metadata?: Record<string, any>;
22
+ }): Promise<{ id: string; status: string }>;
23
+
24
+ /** Send an SMS */
25
+ sendSms(params: {
26
+ to: string;
27
+ body: string;
28
+ metadata?: Record<string, any>;
29
+ }): Promise<{ id: string; status: string }>;
30
+
31
+ /** Send a push notification */
32
+ sendPush(params: {
33
+ token: string;
34
+ title: string;
35
+ body: string;
36
+ data?: Record<string, any>;
37
+ metadata?: Record<string, any>;
38
+ }): Promise<{ id: string; status: string }>;
39
+
40
+ /** Send batch notifications (max 100) */
41
+ sendBatch(notifications: Array<Record<string, any>>): Promise<{
42
+ results: Array<{ index: number; id: string; status: string }>;
43
+ errors: Array<{ index: number; error: string }>;
44
+ total: number;
45
+ succeeded: number;
46
+ failed: number;
47
+ }>;
48
+
49
+ /** List notifications */
50
+ listNotifications(params?: {
51
+ limit?: number;
52
+ offset?: number;
53
+ status?: string;
54
+ }): Promise<any>;
55
+
56
+ /** Check API health */
57
+ health(): Promise<{ ok: boolean }>;
58
+ }
59
+
60
+ declare class NotifyError extends Error {
61
+ status: number;
62
+ response: any;
63
+ constructor(message: string, status: number, response: any);
64
+ }
65
+
66
+ export = NotifySDK;
67
+ export { NotifyError };
package/index.js ADDED
@@ -0,0 +1,267 @@
1
+ const crypto = require("crypto");
2
+
3
+ /**
4
+ * NotifyStack SDK — Production-grade notification client.
5
+ *
6
+ * Supports: Email, SMS, Push notifications
7
+ * Features: Auto-retry, idempotency, batch sending, debug mode, rate limit handling
8
+ *
9
+ * Usage:
10
+ * const NotifySDK = require("notify-saas-sdk");
11
+ * const notify = new NotifySDK("ntf_live_xxxxxxxxx");
12
+ * await notify.track("USER_LOGIN", { email: "user@email.com", name: "Ayush" });
13
+ */
14
+ class NotifySDK {
15
+ /**
16
+ * @param {string} apiKey - Your NotifyStack API key (ntf_live_xxx)
17
+ * @param {object} [options]
18
+ * @param {string} [options.baseUrl="http://localhost:3000"] - API base URL
19
+ * @param {number} [options.maxRetries=3] - Max retry attempts
20
+ * @param {number} [options.timeoutMs=10000] - Request timeout in ms
21
+ * @param {boolean} [options.debug=false] - Enable verbose logging
22
+ */
23
+ constructor(apiKey, options = {}) {
24
+ if (!apiKey || !apiKey.startsWith("ntf_live_")) {
25
+ throw new Error("Invalid API key. Must start with 'ntf_live_'");
26
+ }
27
+ this.apiKey = apiKey;
28
+ this.baseUrl = (options.baseUrl || "http://localhost:3000").replace(/\/$/, "");
29
+ this.maxRetries = options.maxRetries ?? 3;
30
+ this.timeoutMs = options.timeoutMs ?? 10000;
31
+ this.debug = options.debug || false;
32
+ }
33
+
34
+ _log(...args) {
35
+ if (this.debug) console.log("[NotifySDK]", ...args);
36
+ }
37
+
38
+ /**
39
+ * Send an event-based notification using a registered template.
40
+ * @param {string} eventName - Event name (e.g. "USER_LOGIN", "ORDER_PLACED")
41
+ * @param {object} data - Template variables (must include `email`)
42
+ * @param {object} [options] - { metadata, channel, priority }
43
+ * @returns {Promise<{id: string, status: string}>}
44
+ */
45
+ async track(eventName, data, options = {}) {
46
+ if (!eventName) throw new Error("eventName is required");
47
+ if (!data || !data.email) throw new Error("data.email is required for event-based notifications");
48
+
49
+ this._log(`track(${eventName})`, data);
50
+ return this._request("POST", "/v1/notifications", {
51
+ event: eventName,
52
+ data,
53
+ channel: options.channel || "email",
54
+ metadata: options.metadata || options
55
+ });
56
+ }
57
+
58
+ /**
59
+ * Send a direct email notification (no template).
60
+ * @param {object} params
61
+ * @param {string} params.to - Recipient email
62
+ * @param {string} params.subject - Email subject
63
+ * @param {string} params.body - Email body
64
+ * @param {object} [params.metadata]
65
+ * @returns {Promise<{id: string, status: string}>}
66
+ */
67
+ async send({ to, subject, body, metadata }) {
68
+ if (!to || !subject || !body) throw new Error("to, subject, and body are required");
69
+
70
+ this._log(`send() to=${to}`);
71
+ return this._request("POST", "/v1/notifications", {
72
+ recipientEmail: to,
73
+ subject,
74
+ body,
75
+ channel: "email",
76
+ metadata
77
+ });
78
+ }
79
+
80
+ /**
81
+ * Send an SMS notification.
82
+ * @param {object} params
83
+ * @param {string} params.to - Phone number (E.164 format)
84
+ * @param {string} params.body - SMS body (max 1600 chars)
85
+ * @param {object} [params.metadata]
86
+ * @returns {Promise<{id: string, status: string}>}
87
+ */
88
+ async sendSms({ to, body, metadata }) {
89
+ if (!to || !body) throw new Error("to and body are required");
90
+ if (body.length > 1600) throw new Error("SMS body exceeds 1600 character limit");
91
+
92
+ this._log(`sendSms() to=${to}`);
93
+ return this._request("POST", "/v1/notifications", {
94
+ recipientPhone: to,
95
+ subject: "SMS",
96
+ body,
97
+ channel: "sms",
98
+ metadata
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Send a push notification.
104
+ * @param {object} params
105
+ * @param {string} params.token - Device token or subscription JSON
106
+ * @param {string} params.title - Notification title
107
+ * @param {string} params.body - Notification body
108
+ * @param {object} [params.data] - Custom data payload
109
+ * @param {object} [params.metadata]
110
+ * @returns {Promise<{id: string, status: string}>}
111
+ */
112
+ async sendPush({ token, title, body, data, metadata }) {
113
+ if (!token || !title || !body) throw new Error("token, title, and body are required");
114
+
115
+ this._log(`sendPush() title=${title}`);
116
+ return this._request("POST", "/v1/notifications", {
117
+ deviceToken: token,
118
+ subject: title,
119
+ body,
120
+ channel: "push",
121
+ metadata: { ...metadata, pushData: data }
122
+ });
123
+ }
124
+
125
+ /**
126
+ * Send multiple notifications in a batch.
127
+ * @param {Array<object>} notifications - Array of notification payloads
128
+ * @returns {Promise<Array<{id: string, status: string}>>}
129
+ */
130
+ async sendBatch(notifications) {
131
+ if (!Array.isArray(notifications) || !notifications.length) {
132
+ throw new Error("notifications must be a non-empty array");
133
+ }
134
+ if (notifications.length > 100) {
135
+ throw new Error("Batch size cannot exceed 100");
136
+ }
137
+
138
+ this._log(`sendBatch() count=${notifications.length}`);
139
+ const results = [];
140
+ const errors = [];
141
+
142
+ // Process in parallel with concurrency limit of 10
143
+ const chunks = [];
144
+ for (let i = 0; i < notifications.length; i += 10) {
145
+ chunks.push(notifications.slice(i, i + 10));
146
+ }
147
+
148
+ for (const chunk of chunks) {
149
+ const promises = chunk.map(async (n, idx) => {
150
+ try {
151
+ const result = await this._request("POST", "/v1/notifications", n);
152
+ results.push({ index: idx, ...result });
153
+ } catch (e) {
154
+ errors.push({ index: idx, error: e.message });
155
+ }
156
+ });
157
+ await Promise.all(promises);
158
+ }
159
+
160
+ return { results, errors, total: notifications.length, succeeded: results.length, failed: errors.length };
161
+ }
162
+
163
+ /**
164
+ * List notifications for the project.
165
+ * @param {object} [params]
166
+ * @param {number} [params.limit=50]
167
+ * @param {number} [params.offset=0]
168
+ * @param {string} [params.status]
169
+ * @returns {Promise<object>}
170
+ */
171
+ async listNotifications(params = {}) {
172
+ const qs = new URLSearchParams();
173
+ if (params.limit) qs.set("limit", String(params.limit));
174
+ if (params.offset) qs.set("offset", String(params.offset));
175
+ if (params.status) qs.set("status", params.status);
176
+ const query = qs.toString();
177
+ return this._request("GET", `/v1/notifications${query ? "?" + query : ""}`);
178
+ }
179
+
180
+ /**
181
+ * Check API health.
182
+ * @returns {Promise<{ok: boolean}>}
183
+ */
184
+ async health() {
185
+ return this._request("GET", "/health");
186
+ }
187
+
188
+ /**
189
+ * Internal: HTTP request with retry, idempotency, and 429 handling.
190
+ */
191
+ async _request(method, path, body) {
192
+ const idempotencyKey = `idem_${crypto.randomUUID()}`;
193
+ let lastError;
194
+
195
+ for (let attempt = 0; attempt < this.maxRetries; attempt++) {
196
+ const start = Date.now();
197
+ try {
198
+ const url = `${this.baseUrl}${path}`;
199
+ const headers = {
200
+ "Content-Type": "application/json",
201
+ "x-api-key": this.apiKey,
202
+ "x-idempotency-key": idempotencyKey,
203
+ "User-Agent": "NotifyStack-SDK/2.0"
204
+ };
205
+
206
+ const controller = new AbortController();
207
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
208
+
209
+ const fetchOptions = { method, headers, signal: controller.signal };
210
+ if (body && method !== "GET") fetchOptions.body = JSON.stringify(body);
211
+
212
+ const response = await fetch(url, fetchOptions);
213
+ clearTimeout(timeout);
214
+
215
+ const json = await response.json();
216
+ const latency = Date.now() - start;
217
+
218
+ this._log(`${method} ${path} → ${response.status} (${latency}ms)`);
219
+
220
+ if (response.ok) return json.data || json;
221
+
222
+ // Rate limited — wait and retry
223
+ if (response.status === 429) {
224
+ const retryAfter = response.headers.get("retry-after");
225
+ const waitMs = retryAfter ? Number(retryAfter) * 1000 : 5000;
226
+ this._log(`Rate limited. Waiting ${waitMs}ms...`);
227
+ await new Promise(r => setTimeout(r, waitMs));
228
+ continue;
229
+ }
230
+
231
+ // Don't retry client errors (4xx)
232
+ if (response.status >= 400 && response.status < 500) {
233
+ throw new NotifyError(json.message || "Request failed", response.status, json);
234
+ }
235
+
236
+ lastError = new NotifyError(json.message || "Request failed", response.status, json);
237
+ } catch (e) {
238
+ if (e instanceof NotifyError && e.status >= 400 && e.status < 500) throw e;
239
+ lastError = e;
240
+ this._log(`Attempt ${attempt + 1} failed: ${e.message}`);
241
+ }
242
+
243
+ // Exponential backoff with jitter
244
+ if (attempt < this.maxRetries - 1) {
245
+ const base = Math.min(1000 * Math.pow(2, attempt), 10000);
246
+ const jitter = base * 0.2 * (Math.random() * 2 - 1);
247
+ const delay = Math.round(base + jitter);
248
+ this._log(`Retrying in ${delay}ms...`);
249
+ await new Promise(r => setTimeout(r, delay));
250
+ }
251
+ }
252
+
253
+ throw lastError || new Error("Request failed after retries");
254
+ }
255
+ }
256
+
257
+ class NotifyError extends Error {
258
+ constructor(message, status, response) {
259
+ super(message);
260
+ this.name = "NotifyError";
261
+ this.status = status;
262
+ this.response = response;
263
+ }
264
+ }
265
+
266
+ module.exports = NotifySDK;
267
+ module.exports.NotifyError = NotifyError;
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@ayush0x44/notifystack",
3
+ "version": "1.0.0",
4
+ "description": "The official Node.js SDK for NotifyStack — a scalable, production-ready notification SaaS platform.",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/ayush462/notificationSaas.git"
12
+ },
13
+ "keywords": [
14
+ "notifications",
15
+ "email",
16
+ "sms",
17
+ "push",
18
+ "saas",
19
+ "notifystack",
20
+ "node-sdk"
21
+ ],
22
+ "author": "Ayush",
23
+ "license": "MIT",
24
+ "bugs": {
25
+ "url": "https://github.com/ayush462/notificationSaas/issues"
26
+ },
27
+ "homepage": "https://github.com/ayush462/notificationSaas#readme",
28
+ "engines": {
29
+ "node": ">=18.0.0"
30
+ }
31
+ }
package/react.jsx ADDED
@@ -0,0 +1,134 @@
1
+ import React, { useState, useEffect } from "react";
2
+
3
+ /**
4
+ * 🔔 NotifyStack In-App Notification Hook
5
+ */
6
+ export function useNotifyStack(apiKey, userId, options = {}) {
7
+ const [notifications, setNotifications] = useState([]);
8
+ const [unreadCount, setUnreadCount] = useState(0);
9
+ const [loading, setLoading] = useState(false);
10
+ const baseUrl = options.baseUrl || "https://api.notifystack.com";
11
+
12
+ const fetchFeed = async () => {
13
+ if (!userId) return;
14
+ setLoading(true);
15
+ try {
16
+ const res = await fetch(`${baseUrl}/v1/inapp/${userId}`, {
17
+ headers: { "x-api-key": apiKey }
18
+ });
19
+ const json = await res.json();
20
+ if (json.success) {
21
+ setNotifications(json.data.items);
22
+ setUnreadCount(json.data.unreadCount);
23
+ }
24
+ } catch (err) {
25
+ console.error("NotifyStack fetch error:", err);
26
+ } finally {
27
+ setLoading(false);
28
+ }
29
+ };
30
+
31
+ useEffect(() => {
32
+ fetchFeed();
33
+ // Use SSE or poll
34
+ const interval = setInterval(fetchFeed, options.pollingInterval || 15000);
35
+ return () => clearInterval(interval);
36
+ }, [userId, apiKey, baseUrl]);
37
+
38
+ const markAsRead = async (notificationId) => {
39
+ try {
40
+ setUnreadCount(Math.max(0, unreadCount - 1));
41
+ setNotifications(prev => prev.map(n => n.id === notificationId ? { ...n, read_at: new Date().toISOString() } : n));
42
+
43
+ await fetch(`${baseUrl}/v1/inapp/${userId}/read/${notificationId}`, {
44
+ method: "PATCH",
45
+ headers: { "x-api-key": apiKey }
46
+ });
47
+ } catch (err) {
48
+ console.error("Failed to mark as read:", err);
49
+ }
50
+ };
51
+
52
+ const markAllAsRead = async () => {
53
+ try {
54
+ setUnreadCount(0);
55
+ setNotifications(prev => prev.map(n => ({ ...n, read_at: new Date().toISOString() })));
56
+
57
+ await fetch(`${baseUrl}/v1/inapp/${userId}/read`, {
58
+ method: "POST",
59
+ headers: { "x-api-key": apiKey }
60
+ });
61
+ } catch (err) {
62
+ console.error("Failed to mark all as read:", err);
63
+ }
64
+ };
65
+
66
+ return { notifications, unreadCount, loading, markAsRead, markAllAsRead, refresh: fetchFeed };
67
+ }
68
+
69
+ /**
70
+ * 🔔 NotifyStack In-App Bell UI Component (Tailwind required)
71
+ */
72
+ export function NotificationBell({ apiKey, userId, baseUrl }) {
73
+ const [isOpen, setIsOpen] = useState(false);
74
+ const { notifications, unreadCount, markAsRead, markAllAsRead } = useNotifyStack(apiKey, userId, { baseUrl });
75
+
76
+ return (
77
+ <div className="relative">
78
+ <button
79
+ onClick={() => setIsOpen(!isOpen)}
80
+ className="relative p-2 rounded-full hover:bg-gray-100 transition-colors"
81
+ >
82
+ <svg fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="w-6 h-6 text-gray-700">
83
+ <path strokeLinecap="round" strokeLinejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0" />
84
+ </svg>
85
+ {unreadCount > 0 && (
86
+ <div className="absolute top-1 right-1 w-4 h-4 rounded-full bg-red-500 text-[10px] font-bold text-white flex items-center justify-center">
87
+ {unreadCount}
88
+ </div>
89
+ )}
90
+ </button>
91
+
92
+ {isOpen && (
93
+ <div className="absolute right-0 mt-2 w-80 bg-white rounded-xl shadow-2xl border border-gray-100 z-50 overflow-hidden">
94
+ <div className="flex justify-between items-center p-4 border-b border-gray-50 bg-gray-50/50">
95
+ <h3 className="font-semibold text-gray-900">Notifications</h3>
96
+ {unreadCount > 0 && (
97
+ <button onClick={markAllAsRead} className="text-xs text-blue-600 hover:text-blue-800 font-medium">
98
+ Mark all read
99
+ </button>
100
+ )}
101
+ </div>
102
+ <div className="max-h-[400px] overflow-y-auto">
103
+ {notifications.length === 0 ? (
104
+ <div className="p-8 text-center text-gray-500 text-sm">
105
+ You have no notifications yet.
106
+ </div>
107
+ ) : (
108
+ <div className="divide-y divide-gray-50">
109
+ {notifications.map(n => (
110
+ <div
111
+ key={n.id}
112
+ className={`p-4 hover:bg-gray-50 transition-colors cursor-pointer ${!n.read_at && 'bg-blue-50/50'}`}
113
+ onClick={() => !n.read_at && markAsRead(n.id)}
114
+ >
115
+ <div className="flex justify-between items-start mb-1">
116
+ <p className={`text-sm ${!n.read_at ? 'font-semibold text-gray-900' : 'text-gray-700'}`}>
117
+ {n.subject}
118
+ </p>
119
+ {!n.read_at && <div className="w-2 h-2 rounded-full bg-blue-500 mt-1.5 shrink-0" />}
120
+ </div>
121
+ <p className="text-sm text-gray-500 line-clamp-2">{n.body}</p>
122
+ <p className="text-[11px] text-gray-400 mt-2">
123
+ {new Date(n.created_at).toLocaleDateString()}
124
+ </p>
125
+ </div>
126
+ ))}
127
+ </div>
128
+ )}
129
+ </div>
130
+ </div>
131
+ )}
132
+ </div>
133
+ );
134
+ }