sneeksdk 0.3.1

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,134 @@
1
+ # sneeksdk
2
+
3
+ Node.js SDK for the **Sneek Send** API — deliver OTPs and transactional
4
+ messages over SMS, WhatsApp, and email through Sneek's pre-approved DLT
5
+ sender, WhatsApp BSP number, and warmed email domain.
6
+
7
+ > Sneek **delivers**; your app generates and verifies its own OTP codes.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install sneeksdk
13
+ ```
14
+
15
+ ## Quick start
16
+
17
+ ```ts
18
+ import { Sneek } from 'sneeksdk';
19
+
20
+ const sneek = new Sneek({
21
+ apiUrl: process.env.SNEEK_API_URL ?? 'https://api.sneek.in',
22
+ apiKey: process.env.SNEEK_API_KEY!, // snk_live_… or snk_test_…
23
+ });
24
+
25
+ // Generic send (body OR template)
26
+ await sneek.messages.send({
27
+ to: '+919876543210',
28
+ body: 'Your OTP to login to ConfHub is 482917. Do not share it.',
29
+ channel: 'sms', // 'auto' | 'sms' | 'whatsapp' | 'email'
30
+ });
31
+
32
+ // Channel helpers
33
+ await sneek.sendSMS('+919876543210', 'Your code is 482917');
34
+ await sneek.sendWhatsApp('+919876543210', 'Your code is 482917');
35
+ await sneek.sendEmail('user@example.com', 'Your code is 482917');
36
+
37
+ // OTP body-formatting helper (you generate the code yourself)
38
+ await sneek.sendOTP({
39
+ to: '+919876543210',
40
+ code: '482917',
41
+ appName: 'ConfHub',
42
+ channel: 'auto',
43
+ });
44
+
45
+ // DLT/provider templates (server-rendered)
46
+ await sneek.messages.send({
47
+ to: '+919876543210',
48
+ channel: 'sms',
49
+ template: 'otp_login',
50
+ variables: { appName: 'ConfHub', otp: '482917' },
51
+ });
52
+ ```
53
+
54
+ ### Message intent (`type`)
55
+
56
+ Declare whether a message is an OTP or a plain communication so Sneek picks the
57
+ correct approved provider template — instead of guessing from the body text:
58
+
59
+ ```ts
60
+ // Auth / OTP template
61
+ await sneek.messages.send({ to, body: 'Your code is 482917', type: 'otp' });
62
+
63
+ // Utility / communication template
64
+ await sneek.messages.send({
65
+ to,
66
+ body: 'Your booking BK-7781 is confirmed.',
67
+ type: 'transactional',
68
+ });
69
+ ```
70
+
71
+ `type` accepts `'otp'` or `'transactional'`. When omitted, Sneek infers intent
72
+ from the body (which can misclassify digit groups like `BK-7781` as an OTP), so
73
+ **passing `type` explicitly is recommended.** `sendOTP(...)` always tags
74
+ `type: 'otp'` for you.
75
+
76
+ ## Authentication
77
+
78
+ Every request is sent with `Authorization: Bearer <apiKey>`. Mint keys from the
79
+ Sneek admin (`/o/<org>/applications/<id>` → API keys). Keys are shown **once**;
80
+ store them in your environment / secret manager. `snk_test_…` keys hit dev
81
+ adapters; `snk_live_…` keys deliver for real.
82
+
83
+ ## Errors & retries
84
+
85
+ - Non-2xx responses throw `SneekApiError` with `{ status, type, title, detail }`
86
+ (RFC-7807 problem-details).
87
+ - Transient `5xx` and network failures are retried with exponential backoff
88
+ (`maxRetries`, default 2; `retryBaseMs`, default 200ms).
89
+
90
+ ## Idempotency & rate limits
91
+
92
+ Pass `idempotencyKey` to make a send safely repeatable — the first call is
93
+ processed and its result is replayed for any retry with the same key (per
94
+ application), so a dropped connection never sends twice. This pairs with the
95
+ SDK's automatic `5xx` retries.
96
+
97
+ ```ts
98
+ await sneek.messages.send({
99
+ to: '+919876543210',
100
+ type: 'otp',
101
+ body: 'Your code is 482917',
102
+ idempotencyKey: 'order-4815-otp',
103
+ });
104
+ ```
105
+
106
+ Sends are rate-limited per application (your plan's per-minute / per-day quota),
107
+ per recipient, and per source IP. Exceeding a limit throws `SneekApiError` with
108
+ `status === 429`; the problem body carries `retry_after` (seconds).
109
+
110
+
111
+ ```ts
112
+ import { SneekApiError } from 'sneeksdk';
113
+
114
+ try {
115
+ await sneek.sendSMS('+91…', 'hi');
116
+ } catch (err) {
117
+ if (err instanceof SneekApiError && err.status === 401) {
118
+ // invalid / revoked key
119
+ }
120
+ }
121
+ ```
122
+
123
+ ## API
124
+
125
+ | Method | Description |
126
+ | --- | --- |
127
+ | `messages.send(input)` | Send a message (body or template; optional `type` intent). Returns `202` accepted envelope. |
128
+ | `messages.get(id)` | Fetch a previously-sent message by id. |
129
+ | `sendSMS/sendWhatsApp/sendEmail(to, body, clientRef?)` | Channel-pinned helpers. |
130
+ | `sendOTP({ to, code, appName?, channel?, template?, variables? })` | Format + send an OTP body. |
131
+
132
+ ## License
133
+
134
+ UNLICENSED — © Abblor Tech Pvt Ltd.
@@ -0,0 +1,118 @@
1
+ /**
2
+ * sneeksdk — Node.js SDK for the Sneek Send API.
3
+ *
4
+ * const sneek = new Sneek({
5
+ * apiUrl: 'https://api.sneek.in',
6
+ * apiKey: process.env.SNEEK_API_KEY!, // snk_live_… / snk_test_…
7
+ * });
8
+ *
9
+ * await sneek.messages.send({ to: '+91…', body: 'Your OTP is 123456' });
10
+ * await sneek.sendSMS('+91…', 'Your OTP is 123456');
11
+ * await sneek.sendOTP({ to: '+91…', code: '123456', appName: 'ConfHub' });
12
+ *
13
+ * Authenticates with `Authorization: Bearer snk_…` against the server's
14
+ * `BearerApiKeyGuard`. No request signing — the bearer key is the credential.
15
+ */
16
+ interface SneekOptions {
17
+ /** Base URL of the Sneek API, e.g. https://api.sneek.in */
18
+ apiUrl: string;
19
+ /** Bearer API key: `snk_live_…` or `snk_test_…`. */
20
+ apiKey: string;
21
+ /** Override the global fetch (for tests / non-Node runtimes). */
22
+ fetch?: typeof fetch;
23
+ /** Max retry attempts for transient failures (5xx / network). Default 2. */
24
+ maxRetries?: number;
25
+ /** Base backoff in ms between retries (exponential). Default 200. */
26
+ retryBaseMs?: number;
27
+ }
28
+ type MessageChannel = 'sms' | 'whatsapp' | 'email';
29
+ interface SendMessageInput {
30
+ to: string;
31
+ /** Free-form message body. Required unless `template` is given. */
32
+ body?: string;
33
+ channel?: 'auto' | MessageChannel;
34
+ clientRef?: string;
35
+ /**
36
+ * Declares intent so the right approved provider template is used:
37
+ * `otp` → auth/OTP template, `transactional` → utility/communication.
38
+ * When omitted, Sneek infers it from the body text.
39
+ */
40
+ type?: 'otp' | 'transactional';
41
+ /** Named/DLT provider template key (server-rendered). */
42
+ template?: string;
43
+ /** Variables substituted into `template`. */
44
+ variables?: Record<string, string | number>;
45
+ /**
46
+ * Replay-protection key. Two sends with the same key (per application)
47
+ * deliver only once — the first result is returned for every retry. Pair
48
+ * with the SDK's automatic 5xx retries to make sends safely repeatable.
49
+ */
50
+ idempotencyKey?: string;
51
+ }
52
+ interface SendMessageResult {
53
+ id: string;
54
+ status: 'accepted';
55
+ channel: MessageChannel;
56
+ provider: string;
57
+ accepted_at: string;
58
+ client_ref?: string;
59
+ }
60
+ interface SendOtpInput {
61
+ to: string;
62
+ code: string;
63
+ /** App/brand name shown inside the message. */
64
+ appName?: string;
65
+ channel?: 'auto' | MessageChannel;
66
+ clientRef?: string;
67
+ /**
68
+ * Override the default body. Receives `{appName, otp}`. When omitted a
69
+ * sensible default is used. Ignored if `template` is provided.
70
+ */
71
+ body?: (vars: {
72
+ appName: string;
73
+ otp: string;
74
+ }) => string;
75
+ /** DLT/provider template key, when the partner uses server-side rendering. */
76
+ template?: string;
77
+ /** Extra template variables (merged with `{appName, otp}`). */
78
+ variables?: Record<string, string | number>;
79
+ }
80
+ declare class SneekApiError extends Error {
81
+ status: number;
82
+ type: string;
83
+ title: string;
84
+ detail?: string | undefined;
85
+ constructor(status: number, type: string, title: string, detail?: string | undefined);
86
+ }
87
+ declare class Sneek {
88
+ private readonly apiUrl;
89
+ private readonly apiKey;
90
+ private readonly fetchImpl;
91
+ private readonly maxRetries;
92
+ private readonly retryBaseMs;
93
+ readonly messages: MessagesClient;
94
+ constructor(opts: SneekOptions);
95
+ /** @internal — dispatches a bearer-authenticated request with retries. */
96
+ request<T>(method: string, path: string, body?: unknown, extraHeaders?: Record<string, string>): Promise<T>;
97
+ private backoff;
98
+ /** Convenience: send an SMS. */
99
+ sendSMS(to: string, body: string, clientRef?: string): Promise<SendMessageResult>;
100
+ /** Convenience: send a WhatsApp message. */
101
+ sendWhatsApp(to: string, body: string, clientRef?: string): Promise<SendMessageResult>;
102
+ /** Convenience: send an email. */
103
+ sendEmail(to: string, body: string, clientRef?: string): Promise<SendMessageResult>;
104
+ /**
105
+ * OTP body-formatting helper. Sneek only *delivers*; the partner generates
106
+ * and verifies the code. This formats `{appName, otp}` into a body (or
107
+ * forwards a template) and sends it.
108
+ */
109
+ sendOTP(input: SendOtpInput): Promise<SendMessageResult>;
110
+ }
111
+ declare class MessagesClient {
112
+ private readonly sneek;
113
+ constructor(sneek: Sneek);
114
+ send(input: SendMessageInput): Promise<SendMessageResult>;
115
+ get(id: string): Promise<SendMessageResult>;
116
+ }
117
+
118
+ export { type MessageChannel, type SendMessageInput, type SendMessageResult, type SendOtpInput, Sneek, SneekApiError, type SneekOptions, Sneek as default };
@@ -0,0 +1,118 @@
1
+ /**
2
+ * sneeksdk — Node.js SDK for the Sneek Send API.
3
+ *
4
+ * const sneek = new Sneek({
5
+ * apiUrl: 'https://api.sneek.in',
6
+ * apiKey: process.env.SNEEK_API_KEY!, // snk_live_… / snk_test_…
7
+ * });
8
+ *
9
+ * await sneek.messages.send({ to: '+91…', body: 'Your OTP is 123456' });
10
+ * await sneek.sendSMS('+91…', 'Your OTP is 123456');
11
+ * await sneek.sendOTP({ to: '+91…', code: '123456', appName: 'ConfHub' });
12
+ *
13
+ * Authenticates with `Authorization: Bearer snk_…` against the server's
14
+ * `BearerApiKeyGuard`. No request signing — the bearer key is the credential.
15
+ */
16
+ interface SneekOptions {
17
+ /** Base URL of the Sneek API, e.g. https://api.sneek.in */
18
+ apiUrl: string;
19
+ /** Bearer API key: `snk_live_…` or `snk_test_…`. */
20
+ apiKey: string;
21
+ /** Override the global fetch (for tests / non-Node runtimes). */
22
+ fetch?: typeof fetch;
23
+ /** Max retry attempts for transient failures (5xx / network). Default 2. */
24
+ maxRetries?: number;
25
+ /** Base backoff in ms between retries (exponential). Default 200. */
26
+ retryBaseMs?: number;
27
+ }
28
+ type MessageChannel = 'sms' | 'whatsapp' | 'email';
29
+ interface SendMessageInput {
30
+ to: string;
31
+ /** Free-form message body. Required unless `template` is given. */
32
+ body?: string;
33
+ channel?: 'auto' | MessageChannel;
34
+ clientRef?: string;
35
+ /**
36
+ * Declares intent so the right approved provider template is used:
37
+ * `otp` → auth/OTP template, `transactional` → utility/communication.
38
+ * When omitted, Sneek infers it from the body text.
39
+ */
40
+ type?: 'otp' | 'transactional';
41
+ /** Named/DLT provider template key (server-rendered). */
42
+ template?: string;
43
+ /** Variables substituted into `template`. */
44
+ variables?: Record<string, string | number>;
45
+ /**
46
+ * Replay-protection key. Two sends with the same key (per application)
47
+ * deliver only once — the first result is returned for every retry. Pair
48
+ * with the SDK's automatic 5xx retries to make sends safely repeatable.
49
+ */
50
+ idempotencyKey?: string;
51
+ }
52
+ interface SendMessageResult {
53
+ id: string;
54
+ status: 'accepted';
55
+ channel: MessageChannel;
56
+ provider: string;
57
+ accepted_at: string;
58
+ client_ref?: string;
59
+ }
60
+ interface SendOtpInput {
61
+ to: string;
62
+ code: string;
63
+ /** App/brand name shown inside the message. */
64
+ appName?: string;
65
+ channel?: 'auto' | MessageChannel;
66
+ clientRef?: string;
67
+ /**
68
+ * Override the default body. Receives `{appName, otp}`. When omitted a
69
+ * sensible default is used. Ignored if `template` is provided.
70
+ */
71
+ body?: (vars: {
72
+ appName: string;
73
+ otp: string;
74
+ }) => string;
75
+ /** DLT/provider template key, when the partner uses server-side rendering. */
76
+ template?: string;
77
+ /** Extra template variables (merged with `{appName, otp}`). */
78
+ variables?: Record<string, string | number>;
79
+ }
80
+ declare class SneekApiError extends Error {
81
+ status: number;
82
+ type: string;
83
+ title: string;
84
+ detail?: string | undefined;
85
+ constructor(status: number, type: string, title: string, detail?: string | undefined);
86
+ }
87
+ declare class Sneek {
88
+ private readonly apiUrl;
89
+ private readonly apiKey;
90
+ private readonly fetchImpl;
91
+ private readonly maxRetries;
92
+ private readonly retryBaseMs;
93
+ readonly messages: MessagesClient;
94
+ constructor(opts: SneekOptions);
95
+ /** @internal — dispatches a bearer-authenticated request with retries. */
96
+ request<T>(method: string, path: string, body?: unknown, extraHeaders?: Record<string, string>): Promise<T>;
97
+ private backoff;
98
+ /** Convenience: send an SMS. */
99
+ sendSMS(to: string, body: string, clientRef?: string): Promise<SendMessageResult>;
100
+ /** Convenience: send a WhatsApp message. */
101
+ sendWhatsApp(to: string, body: string, clientRef?: string): Promise<SendMessageResult>;
102
+ /** Convenience: send an email. */
103
+ sendEmail(to: string, body: string, clientRef?: string): Promise<SendMessageResult>;
104
+ /**
105
+ * OTP body-formatting helper. Sneek only *delivers*; the partner generates
106
+ * and verifies the code. This formats `{appName, otp}` into a body (or
107
+ * forwards a template) and sends it.
108
+ */
109
+ sendOTP(input: SendOtpInput): Promise<SendMessageResult>;
110
+ }
111
+ declare class MessagesClient {
112
+ private readonly sneek;
113
+ constructor(sneek: Sneek);
114
+ send(input: SendMessageInput): Promise<SendMessageResult>;
115
+ get(id: string): Promise<SendMessageResult>;
116
+ }
117
+
118
+ export { type MessageChannel, type SendMessageInput, type SendMessageResult, type SendOtpInput, Sneek, SneekApiError, type SneekOptions, Sneek as default };
package/dist/index.js ADDED
@@ -0,0 +1,194 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ Sneek: () => Sneek,
24
+ SneekApiError: () => SneekApiError,
25
+ default: () => index_default
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+ var SneekApiError = class extends Error {
29
+ constructor(status, type, title, detail) {
30
+ super(`${title}${detail ? `: ${detail}` : ""} [${status}]`);
31
+ this.status = status;
32
+ this.type = type;
33
+ this.title = title;
34
+ this.detail = detail;
35
+ this.name = "SneekApiError";
36
+ }
37
+ };
38
+ var DEFAULT_MAX_RETRIES = 2;
39
+ var DEFAULT_RETRY_BASE_MS = 200;
40
+ var Sneek = class {
41
+ apiUrl;
42
+ apiKey;
43
+ fetchImpl;
44
+ maxRetries;
45
+ retryBaseMs;
46
+ messages;
47
+ constructor(opts) {
48
+ if (!opts.apiUrl) throw new Error("apiUrl is required");
49
+ if (!opts.apiKey) throw new Error("apiKey is required");
50
+ this.apiUrl = opts.apiUrl.replace(/\/+$/, "");
51
+ this.apiKey = opts.apiKey;
52
+ this.fetchImpl = opts.fetch ?? fetch;
53
+ this.maxRetries = opts.maxRetries ?? DEFAULT_MAX_RETRIES;
54
+ this.retryBaseMs = opts.retryBaseMs ?? DEFAULT_RETRY_BASE_MS;
55
+ this.messages = new MessagesClient(this);
56
+ }
57
+ /** @internal — dispatches a bearer-authenticated request with retries. */
58
+ async request(method, path, body, extraHeaders) {
59
+ const raw = body === void 0 ? void 0 : JSON.stringify(body);
60
+ let attempt = 0;
61
+ let lastErr;
62
+ for (; ; ) {
63
+ try {
64
+ const res = await this.fetchImpl(`${this.apiUrl}${path}`, {
65
+ method,
66
+ headers: {
67
+ "content-type": "application/json",
68
+ authorization: `Bearer ${this.apiKey}`,
69
+ ...extraHeaders ?? {}
70
+ },
71
+ body: raw
72
+ });
73
+ const text = await res.text();
74
+ const parsed = text ? safeJson(text) : void 0;
75
+ if (!res.ok) {
76
+ const p = parsed ?? {};
77
+ const err = new SneekApiError(
78
+ res.status,
79
+ p.type ?? "about:blank",
80
+ p.title ?? res.statusText,
81
+ p.detail
82
+ );
83
+ if (res.status >= 500 && attempt < this.maxRetries) {
84
+ lastErr = err;
85
+ await this.backoff(attempt++);
86
+ continue;
87
+ }
88
+ throw err;
89
+ }
90
+ return parsed;
91
+ } catch (err) {
92
+ if (err instanceof SneekApiError) throw err;
93
+ if (attempt < this.maxRetries) {
94
+ lastErr = err;
95
+ await this.backoff(attempt++);
96
+ continue;
97
+ }
98
+ throw lastErr ?? err;
99
+ }
100
+ }
101
+ }
102
+ backoff(attempt) {
103
+ const ms = this.retryBaseMs * Math.pow(2, attempt);
104
+ return new Promise((resolve) => setTimeout(resolve, ms));
105
+ }
106
+ /** Convenience: send an SMS. */
107
+ sendSMS(to, body, clientRef) {
108
+ return this.messages.send({ to, body, channel: "sms", clientRef });
109
+ }
110
+ /** Convenience: send a WhatsApp message. */
111
+ sendWhatsApp(to, body, clientRef) {
112
+ return this.messages.send({ to, body, channel: "whatsapp", clientRef });
113
+ }
114
+ /** Convenience: send an email. */
115
+ sendEmail(to, body, clientRef) {
116
+ return this.messages.send({ to, body, channel: "email", clientRef });
117
+ }
118
+ /**
119
+ * OTP body-formatting helper. Sneek only *delivers*; the partner generates
120
+ * and verifies the code. This formats `{appName, otp}` into a body (or
121
+ * forwards a template) and sends it.
122
+ */
123
+ sendOTP(input) {
124
+ const appName = input.appName ?? "your account";
125
+ if (input.template) {
126
+ return this.messages.send({
127
+ to: input.to,
128
+ channel: input.channel ?? "auto",
129
+ clientRef: input.clientRef,
130
+ type: "otp",
131
+ template: input.template,
132
+ variables: {
133
+ appName,
134
+ otp: input.code,
135
+ ...input.variables ?? {}
136
+ }
137
+ });
138
+ }
139
+ const body = input.body ? input.body({ appName, otp: input.code }) : `Your OTP to login to ${appName} is ${input.code}. Do not share it with anyone.`;
140
+ return this.messages.send({
141
+ to: input.to,
142
+ body,
143
+ channel: input.channel ?? "auto",
144
+ clientRef: input.clientRef,
145
+ type: "otp"
146
+ });
147
+ }
148
+ };
149
+ var MessagesClient = class {
150
+ constructor(sneek) {
151
+ this.sneek = sneek;
152
+ }
153
+ async send(input) {
154
+ if (!input.body && !input.template) {
155
+ throw new Error("send() requires either `body` or `template`");
156
+ }
157
+ const payload = {
158
+ to: input.to,
159
+ channel: input.channel ?? "auto"
160
+ };
161
+ if (input.body !== void 0) payload.body = input.body;
162
+ if (input.type !== void 0) payload.type = input.type;
163
+ if (input.template !== void 0) payload.template = input.template;
164
+ if (input.variables !== void 0) payload.variables = input.variables;
165
+ if (input.clientRef) payload.client_ref = input.clientRef;
166
+ const headers = input.idempotencyKey ? { "idempotency-key": input.idempotencyKey } : void 0;
167
+ return this.sneek.request(
168
+ "POST",
169
+ "/api/messages/send",
170
+ payload,
171
+ headers
172
+ );
173
+ }
174
+ async get(id) {
175
+ return this.sneek.request(
176
+ "GET",
177
+ `/api/messages/${encodeURIComponent(id)}`
178
+ );
179
+ }
180
+ };
181
+ function safeJson(s) {
182
+ try {
183
+ return JSON.parse(s);
184
+ } catch {
185
+ return void 0;
186
+ }
187
+ }
188
+ var index_default = Sneek;
189
+ // Annotate the CommonJS export names for ESM import in node:
190
+ 0 && (module.exports = {
191
+ Sneek,
192
+ SneekApiError
193
+ });
194
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * sneeksdk — Node.js SDK for the Sneek Send API.\n *\n * const sneek = new Sneek({\n * apiUrl: 'https://api.sneek.in',\n * apiKey: process.env.SNEEK_API_KEY!, // snk_live_… / snk_test_…\n * });\n *\n * await sneek.messages.send({ to: '+91…', body: 'Your OTP is 123456' });\n * await sneek.sendSMS('+91…', 'Your OTP is 123456');\n * await sneek.sendOTP({ to: '+91…', code: '123456', appName: 'ConfHub' });\n *\n * Authenticates with `Authorization: Bearer snk_…` against the server's\n * `BearerApiKeyGuard`. No request signing — the bearer key is the credential.\n */\n\nexport interface SneekOptions {\n /** Base URL of the Sneek API, e.g. https://api.sneek.in */\n apiUrl: string;\n /** Bearer API key: `snk_live_…` or `snk_test_…`. */\n apiKey: string;\n /** Override the global fetch (for tests / non-Node runtimes). */\n fetch?: typeof fetch;\n /** Max retry attempts for transient failures (5xx / network). Default 2. */\n maxRetries?: number;\n /** Base backoff in ms between retries (exponential). Default 200. */\n retryBaseMs?: number;\n}\n\nexport type MessageChannel = 'sms' | 'whatsapp' | 'email';\n\nexport interface SendMessageInput {\n to: string;\n /** Free-form message body. Required unless `template` is given. */\n body?: string;\n channel?: 'auto' | MessageChannel;\n clientRef?: string;\n /**\n * Declares intent so the right approved provider template is used:\n * `otp` → auth/OTP template, `transactional` → utility/communication.\n * When omitted, Sneek infers it from the body text.\n */\n type?: 'otp' | 'transactional';\n /** Named/DLT provider template key (server-rendered). */\n template?: string;\n /** Variables substituted into `template`. */\n variables?: Record<string, string | number>;\n /**\n * Replay-protection key. Two sends with the same key (per application)\n * deliver only once — the first result is returned for every retry. Pair\n * with the SDK's automatic 5xx retries to make sends safely repeatable.\n */\n idempotencyKey?: string;\n}\n\nexport interface SendMessageResult {\n id: string;\n status: 'accepted';\n channel: MessageChannel;\n provider: string;\n accepted_at: string;\n client_ref?: string;\n}\n\nexport interface SendOtpInput {\n to: string;\n code: string;\n /** App/brand name shown inside the message. */\n appName?: string;\n channel?: 'auto' | MessageChannel;\n clientRef?: string;\n /**\n * Override the default body. Receives `{appName, otp}`. When omitted a\n * sensible default is used. Ignored if `template` is provided.\n */\n body?: (vars: { appName: string; otp: string }) => string;\n /** DLT/provider template key, when the partner uses server-side rendering. */\n template?: string;\n /** Extra template variables (merged with `{appName, otp}`). */\n variables?: Record<string, string | number>;\n}\n\nexport class SneekApiError extends Error {\n constructor(\n public status: number,\n public type: string,\n public title: string,\n public detail?: string,\n ) {\n super(`${title}${detail ? `: ${detail}` : ''} [${status}]`);\n this.name = 'SneekApiError';\n }\n}\n\nconst DEFAULT_MAX_RETRIES = 2;\nconst DEFAULT_RETRY_BASE_MS = 200;\n\nexport class Sneek {\n private readonly apiUrl: string;\n private readonly apiKey: string;\n private readonly fetchImpl: typeof fetch;\n private readonly maxRetries: number;\n private readonly retryBaseMs: number;\n\n readonly messages: MessagesClient;\n\n constructor(opts: SneekOptions) {\n if (!opts.apiUrl) throw new Error('apiUrl is required');\n if (!opts.apiKey) throw new Error('apiKey is required');\n this.apiUrl = opts.apiUrl.replace(/\\/+$/, '');\n this.apiKey = opts.apiKey;\n this.fetchImpl = opts.fetch ?? fetch;\n this.maxRetries = opts.maxRetries ?? DEFAULT_MAX_RETRIES;\n this.retryBaseMs = opts.retryBaseMs ?? DEFAULT_RETRY_BASE_MS;\n this.messages = new MessagesClient(this);\n }\n\n /** @internal — dispatches a bearer-authenticated request with retries. */\n async request<T>(\n method: string,\n path: string,\n body?: unknown,\n extraHeaders?: Record<string, string>,\n ): Promise<T> {\n const raw = body === undefined ? undefined : JSON.stringify(body);\n let attempt = 0;\n let lastErr: unknown;\n for (;;) {\n try {\n const res = await this.fetchImpl(`${this.apiUrl}${path}`, {\n method,\n headers: {\n 'content-type': 'application/json',\n authorization: `Bearer ${this.apiKey}`,\n ...(extraHeaders ?? {}),\n },\n body: raw,\n });\n const text = await res.text();\n const parsed: unknown = text ? safeJson(text) : undefined;\n if (!res.ok) {\n const p = (parsed ?? {}) as {\n type?: string;\n title?: string;\n detail?: string;\n };\n const err = new SneekApiError(\n res.status,\n p.type ?? 'about:blank',\n p.title ?? res.statusText,\n p.detail,\n );\n if (res.status >= 500 && attempt < this.maxRetries) {\n lastErr = err;\n await this.backoff(attempt++);\n continue;\n }\n throw err;\n }\n return parsed as T;\n } catch (err) {\n if (err instanceof SneekApiError) throw err;\n if (attempt < this.maxRetries) {\n lastErr = err;\n await this.backoff(attempt++);\n continue;\n }\n throw lastErr ?? err;\n }\n }\n }\n\n private backoff(attempt: number): Promise<void> {\n const ms = this.retryBaseMs * Math.pow(2, attempt);\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n\n /** Convenience: send an SMS. */\n sendSMS(to: string, body: string, clientRef?: string) {\n return this.messages.send({ to, body, channel: 'sms', clientRef });\n }\n\n /** Convenience: send a WhatsApp message. */\n sendWhatsApp(to: string, body: string, clientRef?: string) {\n return this.messages.send({ to, body, channel: 'whatsapp', clientRef });\n }\n\n /** Convenience: send an email. */\n sendEmail(to: string, body: string, clientRef?: string) {\n return this.messages.send({ to, body, channel: 'email', clientRef });\n }\n\n /**\n * OTP body-formatting helper. Sneek only *delivers*; the partner generates\n * and verifies the code. This formats `{appName, otp}` into a body (or\n * forwards a template) and sends it.\n */\n sendOTP(input: SendOtpInput): Promise<SendMessageResult> {\n const appName = input.appName ?? 'your account';\n if (input.template) {\n return this.messages.send({\n to: input.to,\n channel: input.channel ?? 'auto',\n clientRef: input.clientRef,\n type: 'otp',\n template: input.template,\n variables: {\n appName,\n otp: input.code,\n ...(input.variables ?? {}),\n },\n });\n }\n const body = input.body\n ? input.body({ appName, otp: input.code })\n : `Your OTP to login to ${appName} is ${input.code}. Do not share it with anyone.`;\n return this.messages.send({\n to: input.to,\n body,\n channel: input.channel ?? 'auto',\n clientRef: input.clientRef,\n type: 'otp',\n });\n }\n}\n\nclass MessagesClient {\n constructor(private readonly sneek: Sneek) {}\n\n async send(input: SendMessageInput): Promise<SendMessageResult> {\n if (!input.body && !input.template) {\n throw new Error('send() requires either `body` or `template`');\n }\n const payload: Record<string, unknown> = {\n to: input.to,\n channel: input.channel ?? 'auto',\n };\n if (input.body !== undefined) payload.body = input.body;\n if (input.type !== undefined) payload.type = input.type;\n if (input.template !== undefined) payload.template = input.template;\n if (input.variables !== undefined) payload.variables = input.variables;\n if (input.clientRef) payload.client_ref = input.clientRef;\n const headers = input.idempotencyKey\n ? { 'idempotency-key': input.idempotencyKey }\n : undefined;\n return this.sneek.request<SendMessageResult>(\n 'POST',\n '/api/messages/send',\n payload,\n headers,\n );\n }\n\n async get(id: string): Promise<SendMessageResult> {\n return this.sneek.request<SendMessageResult>(\n 'GET',\n `/api/messages/${encodeURIComponent(id)}`,\n );\n }\n}\n\nfunction safeJson(s: string): unknown {\n try {\n return JSON.parse(s);\n } catch {\n return undefined;\n }\n}\n\nexport default Sneek;\n"],"mappings":"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","names":[]}
package/dist/index.mjs ADDED
@@ -0,0 +1,168 @@
1
+ // src/index.ts
2
+ var SneekApiError = class extends Error {
3
+ constructor(status, type, title, detail) {
4
+ super(`${title}${detail ? `: ${detail}` : ""} [${status}]`);
5
+ this.status = status;
6
+ this.type = type;
7
+ this.title = title;
8
+ this.detail = detail;
9
+ this.name = "SneekApiError";
10
+ }
11
+ };
12
+ var DEFAULT_MAX_RETRIES = 2;
13
+ var DEFAULT_RETRY_BASE_MS = 200;
14
+ var Sneek = class {
15
+ apiUrl;
16
+ apiKey;
17
+ fetchImpl;
18
+ maxRetries;
19
+ retryBaseMs;
20
+ messages;
21
+ constructor(opts) {
22
+ if (!opts.apiUrl) throw new Error("apiUrl is required");
23
+ if (!opts.apiKey) throw new Error("apiKey is required");
24
+ this.apiUrl = opts.apiUrl.replace(/\/+$/, "");
25
+ this.apiKey = opts.apiKey;
26
+ this.fetchImpl = opts.fetch ?? fetch;
27
+ this.maxRetries = opts.maxRetries ?? DEFAULT_MAX_RETRIES;
28
+ this.retryBaseMs = opts.retryBaseMs ?? DEFAULT_RETRY_BASE_MS;
29
+ this.messages = new MessagesClient(this);
30
+ }
31
+ /** @internal — dispatches a bearer-authenticated request with retries. */
32
+ async request(method, path, body, extraHeaders) {
33
+ const raw = body === void 0 ? void 0 : JSON.stringify(body);
34
+ let attempt = 0;
35
+ let lastErr;
36
+ for (; ; ) {
37
+ try {
38
+ const res = await this.fetchImpl(`${this.apiUrl}${path}`, {
39
+ method,
40
+ headers: {
41
+ "content-type": "application/json",
42
+ authorization: `Bearer ${this.apiKey}`,
43
+ ...extraHeaders ?? {}
44
+ },
45
+ body: raw
46
+ });
47
+ const text = await res.text();
48
+ const parsed = text ? safeJson(text) : void 0;
49
+ if (!res.ok) {
50
+ const p = parsed ?? {};
51
+ const err = new SneekApiError(
52
+ res.status,
53
+ p.type ?? "about:blank",
54
+ p.title ?? res.statusText,
55
+ p.detail
56
+ );
57
+ if (res.status >= 500 && attempt < this.maxRetries) {
58
+ lastErr = err;
59
+ await this.backoff(attempt++);
60
+ continue;
61
+ }
62
+ throw err;
63
+ }
64
+ return parsed;
65
+ } catch (err) {
66
+ if (err instanceof SneekApiError) throw err;
67
+ if (attempt < this.maxRetries) {
68
+ lastErr = err;
69
+ await this.backoff(attempt++);
70
+ continue;
71
+ }
72
+ throw lastErr ?? err;
73
+ }
74
+ }
75
+ }
76
+ backoff(attempt) {
77
+ const ms = this.retryBaseMs * Math.pow(2, attempt);
78
+ return new Promise((resolve) => setTimeout(resolve, ms));
79
+ }
80
+ /** Convenience: send an SMS. */
81
+ sendSMS(to, body, clientRef) {
82
+ return this.messages.send({ to, body, channel: "sms", clientRef });
83
+ }
84
+ /** Convenience: send a WhatsApp message. */
85
+ sendWhatsApp(to, body, clientRef) {
86
+ return this.messages.send({ to, body, channel: "whatsapp", clientRef });
87
+ }
88
+ /** Convenience: send an email. */
89
+ sendEmail(to, body, clientRef) {
90
+ return this.messages.send({ to, body, channel: "email", clientRef });
91
+ }
92
+ /**
93
+ * OTP body-formatting helper. Sneek only *delivers*; the partner generates
94
+ * and verifies the code. This formats `{appName, otp}` into a body (or
95
+ * forwards a template) and sends it.
96
+ */
97
+ sendOTP(input) {
98
+ const appName = input.appName ?? "your account";
99
+ if (input.template) {
100
+ return this.messages.send({
101
+ to: input.to,
102
+ channel: input.channel ?? "auto",
103
+ clientRef: input.clientRef,
104
+ type: "otp",
105
+ template: input.template,
106
+ variables: {
107
+ appName,
108
+ otp: input.code,
109
+ ...input.variables ?? {}
110
+ }
111
+ });
112
+ }
113
+ const body = input.body ? input.body({ appName, otp: input.code }) : `Your OTP to login to ${appName} is ${input.code}. Do not share it with anyone.`;
114
+ return this.messages.send({
115
+ to: input.to,
116
+ body,
117
+ channel: input.channel ?? "auto",
118
+ clientRef: input.clientRef,
119
+ type: "otp"
120
+ });
121
+ }
122
+ };
123
+ var MessagesClient = class {
124
+ constructor(sneek) {
125
+ this.sneek = sneek;
126
+ }
127
+ async send(input) {
128
+ if (!input.body && !input.template) {
129
+ throw new Error("send() requires either `body` or `template`");
130
+ }
131
+ const payload = {
132
+ to: input.to,
133
+ channel: input.channel ?? "auto"
134
+ };
135
+ if (input.body !== void 0) payload.body = input.body;
136
+ if (input.type !== void 0) payload.type = input.type;
137
+ if (input.template !== void 0) payload.template = input.template;
138
+ if (input.variables !== void 0) payload.variables = input.variables;
139
+ if (input.clientRef) payload.client_ref = input.clientRef;
140
+ const headers = input.idempotencyKey ? { "idempotency-key": input.idempotencyKey } : void 0;
141
+ return this.sneek.request(
142
+ "POST",
143
+ "/api/messages/send",
144
+ payload,
145
+ headers
146
+ );
147
+ }
148
+ async get(id) {
149
+ return this.sneek.request(
150
+ "GET",
151
+ `/api/messages/${encodeURIComponent(id)}`
152
+ );
153
+ }
154
+ };
155
+ function safeJson(s) {
156
+ try {
157
+ return JSON.parse(s);
158
+ } catch {
159
+ return void 0;
160
+ }
161
+ }
162
+ var index_default = Sneek;
163
+ export {
164
+ Sneek,
165
+ SneekApiError,
166
+ index_default as default
167
+ };
168
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * sneeksdk — Node.js SDK for the Sneek Send API.\n *\n * const sneek = new Sneek({\n * apiUrl: 'https://api.sneek.in',\n * apiKey: process.env.SNEEK_API_KEY!, // snk_live_… / snk_test_…\n * });\n *\n * await sneek.messages.send({ to: '+91…', body: 'Your OTP is 123456' });\n * await sneek.sendSMS('+91…', 'Your OTP is 123456');\n * await sneek.sendOTP({ to: '+91…', code: '123456', appName: 'ConfHub' });\n *\n * Authenticates with `Authorization: Bearer snk_…` against the server's\n * `BearerApiKeyGuard`. No request signing — the bearer key is the credential.\n */\n\nexport interface SneekOptions {\n /** Base URL of the Sneek API, e.g. https://api.sneek.in */\n apiUrl: string;\n /** Bearer API key: `snk_live_…` or `snk_test_…`. */\n apiKey: string;\n /** Override the global fetch (for tests / non-Node runtimes). */\n fetch?: typeof fetch;\n /** Max retry attempts for transient failures (5xx / network). Default 2. */\n maxRetries?: number;\n /** Base backoff in ms between retries (exponential). Default 200. */\n retryBaseMs?: number;\n}\n\nexport type MessageChannel = 'sms' | 'whatsapp' | 'email';\n\nexport interface SendMessageInput {\n to: string;\n /** Free-form message body. Required unless `template` is given. */\n body?: string;\n channel?: 'auto' | MessageChannel;\n clientRef?: string;\n /**\n * Declares intent so the right approved provider template is used:\n * `otp` → auth/OTP template, `transactional` → utility/communication.\n * When omitted, Sneek infers it from the body text.\n */\n type?: 'otp' | 'transactional';\n /** Named/DLT provider template key (server-rendered). */\n template?: string;\n /** Variables substituted into `template`. */\n variables?: Record<string, string | number>;\n /**\n * Replay-protection key. Two sends with the same key (per application)\n * deliver only once — the first result is returned for every retry. Pair\n * with the SDK's automatic 5xx retries to make sends safely repeatable.\n */\n idempotencyKey?: string;\n}\n\nexport interface SendMessageResult {\n id: string;\n status: 'accepted';\n channel: MessageChannel;\n provider: string;\n accepted_at: string;\n client_ref?: string;\n}\n\nexport interface SendOtpInput {\n to: string;\n code: string;\n /** App/brand name shown inside the message. */\n appName?: string;\n channel?: 'auto' | MessageChannel;\n clientRef?: string;\n /**\n * Override the default body. Receives `{appName, otp}`. When omitted a\n * sensible default is used. Ignored if `template` is provided.\n */\n body?: (vars: { appName: string; otp: string }) => string;\n /** DLT/provider template key, when the partner uses server-side rendering. */\n template?: string;\n /** Extra template variables (merged with `{appName, otp}`). */\n variables?: Record<string, string | number>;\n}\n\nexport class SneekApiError extends Error {\n constructor(\n public status: number,\n public type: string,\n public title: string,\n public detail?: string,\n ) {\n super(`${title}${detail ? `: ${detail}` : ''} [${status}]`);\n this.name = 'SneekApiError';\n }\n}\n\nconst DEFAULT_MAX_RETRIES = 2;\nconst DEFAULT_RETRY_BASE_MS = 200;\n\nexport class Sneek {\n private readonly apiUrl: string;\n private readonly apiKey: string;\n private readonly fetchImpl: typeof fetch;\n private readonly maxRetries: number;\n private readonly retryBaseMs: number;\n\n readonly messages: MessagesClient;\n\n constructor(opts: SneekOptions) {\n if (!opts.apiUrl) throw new Error('apiUrl is required');\n if (!opts.apiKey) throw new Error('apiKey is required');\n this.apiUrl = opts.apiUrl.replace(/\\/+$/, '');\n this.apiKey = opts.apiKey;\n this.fetchImpl = opts.fetch ?? fetch;\n this.maxRetries = opts.maxRetries ?? DEFAULT_MAX_RETRIES;\n this.retryBaseMs = opts.retryBaseMs ?? DEFAULT_RETRY_BASE_MS;\n this.messages = new MessagesClient(this);\n }\n\n /** @internal — dispatches a bearer-authenticated request with retries. */\n async request<T>(\n method: string,\n path: string,\n body?: unknown,\n extraHeaders?: Record<string, string>,\n ): Promise<T> {\n const raw = body === undefined ? undefined : JSON.stringify(body);\n let attempt = 0;\n let lastErr: unknown;\n for (;;) {\n try {\n const res = await this.fetchImpl(`${this.apiUrl}${path}`, {\n method,\n headers: {\n 'content-type': 'application/json',\n authorization: `Bearer ${this.apiKey}`,\n ...(extraHeaders ?? {}),\n },\n body: raw,\n });\n const text = await res.text();\n const parsed: unknown = text ? safeJson(text) : undefined;\n if (!res.ok) {\n const p = (parsed ?? {}) as {\n type?: string;\n title?: string;\n detail?: string;\n };\n const err = new SneekApiError(\n res.status,\n p.type ?? 'about:blank',\n p.title ?? res.statusText,\n p.detail,\n );\n if (res.status >= 500 && attempt < this.maxRetries) {\n lastErr = err;\n await this.backoff(attempt++);\n continue;\n }\n throw err;\n }\n return parsed as T;\n } catch (err) {\n if (err instanceof SneekApiError) throw err;\n if (attempt < this.maxRetries) {\n lastErr = err;\n await this.backoff(attempt++);\n continue;\n }\n throw lastErr ?? err;\n }\n }\n }\n\n private backoff(attempt: number): Promise<void> {\n const ms = this.retryBaseMs * Math.pow(2, attempt);\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n\n /** Convenience: send an SMS. */\n sendSMS(to: string, body: string, clientRef?: string) {\n return this.messages.send({ to, body, channel: 'sms', clientRef });\n }\n\n /** Convenience: send a WhatsApp message. */\n sendWhatsApp(to: string, body: string, clientRef?: string) {\n return this.messages.send({ to, body, channel: 'whatsapp', clientRef });\n }\n\n /** Convenience: send an email. */\n sendEmail(to: string, body: string, clientRef?: string) {\n return this.messages.send({ to, body, channel: 'email', clientRef });\n }\n\n /**\n * OTP body-formatting helper. Sneek only *delivers*; the partner generates\n * and verifies the code. This formats `{appName, otp}` into a body (or\n * forwards a template) and sends it.\n */\n sendOTP(input: SendOtpInput): Promise<SendMessageResult> {\n const appName = input.appName ?? 'your account';\n if (input.template) {\n return this.messages.send({\n to: input.to,\n channel: input.channel ?? 'auto',\n clientRef: input.clientRef,\n type: 'otp',\n template: input.template,\n variables: {\n appName,\n otp: input.code,\n ...(input.variables ?? {}),\n },\n });\n }\n const body = input.body\n ? input.body({ appName, otp: input.code })\n : `Your OTP to login to ${appName} is ${input.code}. Do not share it with anyone.`;\n return this.messages.send({\n to: input.to,\n body,\n channel: input.channel ?? 'auto',\n clientRef: input.clientRef,\n type: 'otp',\n });\n }\n}\n\nclass MessagesClient {\n constructor(private readonly sneek: Sneek) {}\n\n async send(input: SendMessageInput): Promise<SendMessageResult> {\n if (!input.body && !input.template) {\n throw new Error('send() requires either `body` or `template`');\n }\n const payload: Record<string, unknown> = {\n to: input.to,\n channel: input.channel ?? 'auto',\n };\n if (input.body !== undefined) payload.body = input.body;\n if (input.type !== undefined) payload.type = input.type;\n if (input.template !== undefined) payload.template = input.template;\n if (input.variables !== undefined) payload.variables = input.variables;\n if (input.clientRef) payload.client_ref = input.clientRef;\n const headers = input.idempotencyKey\n ? { 'idempotency-key': input.idempotencyKey }\n : undefined;\n return this.sneek.request<SendMessageResult>(\n 'POST',\n '/api/messages/send',\n payload,\n headers,\n );\n }\n\n async get(id: string): Promise<SendMessageResult> {\n return this.sneek.request<SendMessageResult>(\n 'GET',\n `/api/messages/${encodeURIComponent(id)}`,\n );\n }\n}\n\nfunction safeJson(s: string): unknown {\n try {\n return JSON.parse(s);\n } catch {\n return undefined;\n }\n}\n\nexport default Sneek;\n"],"mappings":";AAkFO,IAAM,gBAAN,cAA4B,MAAM;AAAA,EACrC,YACW,QACA,MACA,OACA,QACT;AACE,UAAM,GAAG,KAAK,GAAG,SAAS,KAAK,MAAM,KAAK,EAAE,KAAK,MAAM,GAAG;AALnD;AACA;AACA;AACA;AAGP,SAAK,OAAO;AAAA,EAChB;AACJ;AAEA,IAAM,sBAAsB;AAC5B,IAAM,wBAAwB;AAEvB,IAAM,QAAN,MAAY;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAER;AAAA,EAET,YAAY,MAAoB;AAC5B,QAAI,CAAC,KAAK,OAAQ,OAAM,IAAI,MAAM,oBAAoB;AACtD,QAAI,CAAC,KAAK,OAAQ,OAAM,IAAI,MAAM,oBAAoB;AACtD,SAAK,SAAS,KAAK,OAAO,QAAQ,QAAQ,EAAE;AAC5C,SAAK,SAAS,KAAK;AACnB,SAAK,YAAY,KAAK,SAAS;AAC/B,SAAK,aAAa,KAAK,cAAc;AACrC,SAAK,cAAc,KAAK,eAAe;AACvC,SAAK,WAAW,IAAI,eAAe,IAAI;AAAA,EAC3C;AAAA;AAAA,EAGA,MAAM,QACF,QACA,MACA,MACA,cACU;AACV,UAAM,MAAM,SAAS,SAAY,SAAY,KAAK,UAAU,IAAI;AAChE,QAAI,UAAU;AACd,QAAI;AACJ,eAAS;AACL,UAAI;AACA,cAAM,MAAM,MAAM,KAAK,UAAU,GAAG,KAAK,MAAM,GAAG,IAAI,IAAI;AAAA,UACtD;AAAA,UACA,SAAS;AAAA,YACL,gBAAgB;AAAA,YAChB,eAAe,UAAU,KAAK,MAAM;AAAA,YACpC,GAAI,gBAAgB,CAAC;AAAA,UACzB;AAAA,UACA,MAAM;AAAA,QACV,CAAC;AACD,cAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,cAAM,SAAkB,OAAO,SAAS,IAAI,IAAI;AAChD,YAAI,CAAC,IAAI,IAAI;AACT,gBAAM,IAAK,UAAU,CAAC;AAKtB,gBAAM,MAAM,IAAI;AAAA,YACZ,IAAI;AAAA,YACJ,EAAE,QAAQ;AAAA,YACV,EAAE,SAAS,IAAI;AAAA,YACf,EAAE;AAAA,UACN;AACA,cAAI,IAAI,UAAU,OAAO,UAAU,KAAK,YAAY;AAChD,sBAAU;AACV,kBAAM,KAAK,QAAQ,SAAS;AAC5B;AAAA,UACJ;AACA,gBAAM;AAAA,QACV;AACA,eAAO;AAAA,MACX,SAAS,KAAK;AACV,YAAI,eAAe,cAAe,OAAM;AACxC,YAAI,UAAU,KAAK,YAAY;AAC3B,oBAAU;AACV,gBAAM,KAAK,QAAQ,SAAS;AAC5B;AAAA,QACJ;AACA,cAAM,WAAW;AAAA,MACrB;AAAA,IACJ;AAAA,EACJ;AAAA,EAEQ,QAAQ,SAAgC;AAC5C,UAAM,KAAK,KAAK,cAAc,KAAK,IAAI,GAAG,OAAO;AACjD,WAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AAAA,EAC3D;AAAA;AAAA,EAGA,QAAQ,IAAY,MAAc,WAAoB;AAClD,WAAO,KAAK,SAAS,KAAK,EAAE,IAAI,MAAM,SAAS,OAAO,UAAU,CAAC;AAAA,EACrE;AAAA;AAAA,EAGA,aAAa,IAAY,MAAc,WAAoB;AACvD,WAAO,KAAK,SAAS,KAAK,EAAE,IAAI,MAAM,SAAS,YAAY,UAAU,CAAC;AAAA,EAC1E;AAAA;AAAA,EAGA,UAAU,IAAY,MAAc,WAAoB;AACpD,WAAO,KAAK,SAAS,KAAK,EAAE,IAAI,MAAM,SAAS,SAAS,UAAU,CAAC;AAAA,EACvE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,QAAQ,OAAiD;AACrD,UAAM,UAAU,MAAM,WAAW;AACjC,QAAI,MAAM,UAAU;AAChB,aAAO,KAAK,SAAS,KAAK;AAAA,QACtB,IAAI,MAAM;AAAA,QACV,SAAS,MAAM,WAAW;AAAA,QAC1B,WAAW,MAAM;AAAA,QACjB,MAAM;AAAA,QACN,UAAU,MAAM;AAAA,QAChB,WAAW;AAAA,UACP;AAAA,UACA,KAAK,MAAM;AAAA,UACX,GAAI,MAAM,aAAa,CAAC;AAAA,QAC5B;AAAA,MACJ,CAAC;AAAA,IACL;AACA,UAAM,OAAO,MAAM,OACb,MAAM,KAAK,EAAE,SAAS,KAAK,MAAM,KAAK,CAAC,IACvC,wBAAwB,OAAO,OAAO,MAAM,IAAI;AACtD,WAAO,KAAK,SAAS,KAAK;AAAA,MACtB,IAAI,MAAM;AAAA,MACV;AAAA,MACA,SAAS,MAAM,WAAW;AAAA,MAC1B,WAAW,MAAM;AAAA,MACjB,MAAM;AAAA,IACV,CAAC;AAAA,EACL;AACJ;AAEA,IAAM,iBAAN,MAAqB;AAAA,EACjB,YAA6B,OAAc;AAAd;AAAA,EAAe;AAAA,EAE5C,MAAM,KAAK,OAAqD;AAC5D,QAAI,CAAC,MAAM,QAAQ,CAAC,MAAM,UAAU;AAChC,YAAM,IAAI,MAAM,6CAA6C;AAAA,IACjE;AACA,UAAM,UAAmC;AAAA,MACrC,IAAI,MAAM;AAAA,MACV,SAAS,MAAM,WAAW;AAAA,IAC9B;AACA,QAAI,MAAM,SAAS,OAAW,SAAQ,OAAO,MAAM;AACnD,QAAI,MAAM,SAAS,OAAW,SAAQ,OAAO,MAAM;AACnD,QAAI,MAAM,aAAa,OAAW,SAAQ,WAAW,MAAM;AAC3D,QAAI,MAAM,cAAc,OAAW,SAAQ,YAAY,MAAM;AAC7D,QAAI,MAAM,UAAW,SAAQ,aAAa,MAAM;AAChD,UAAM,UAAU,MAAM,iBAChB,EAAE,mBAAmB,MAAM,eAAe,IAC1C;AACN,WAAO,KAAK,MAAM;AAAA,MACd;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACJ;AAAA,EACJ;AAAA,EAEA,MAAM,IAAI,IAAwC;AAC9C,WAAO,KAAK,MAAM;AAAA,MACd;AAAA,MACA,iBAAiB,mBAAmB,EAAE,CAAC;AAAA,IAC3C;AAAA,EACJ;AACJ;AAEA,SAAS,SAAS,GAAoB;AAClC,MAAI;AACA,WAAO,KAAK,MAAM,CAAC;AAAA,EACvB,QAAQ;AACJ,WAAO;AAAA,EACX;AACJ;AAEA,IAAO,gBAAQ;","names":[]}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "sneeksdk",
3
+ "version": "0.3.1",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "description": "Node.js SDK for the Sneek Messaging API",
8
+ "homepage": "https://sneek.in/docs",
9
+ "bugs": {
10
+ "url": "https://sneek.in/docs"
11
+ },
12
+ "main": "dist/index.js",
13
+ "module": "dist/index.mjs",
14
+ "types": "dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.mjs",
19
+ "require": "./dist/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "scripts": {
26
+ "build": "tsup",
27
+ "type-check": "tsc --noEmit",
28
+ "test": "node --test --import tsx test/*.test.ts"
29
+ },
30
+ "keywords": [
31
+ "sneek",
32
+ "messaging",
33
+ "sms",
34
+ "whatsapp",
35
+ "email",
36
+ "otp"
37
+ ],
38
+ "author": "Abblor Tech Pvt Ltd <abblorltd@gmail.com> (https://sneek.in)",
39
+ "license": "UNLICENSED",
40
+ "devDependencies": {
41
+ "@types/node": "^20.11.0",
42
+ "tsup": "^8.0.0",
43
+ "tsx": "^4.7.0",
44
+ "typescript": "^5.3.3"
45
+ },
46
+ "engines": {
47
+ "node": ">=18"
48
+ }
49
+ }