@narcisbodea/smstunnel-sdk 1.1.0 → 1.1.2

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.
@@ -0,0 +1,228 @@
1
+ /**
2
+ * SMSTunnel Node.js Client
3
+ *
4
+ * Standalone client that connects directly to the SMSTunnel API.
5
+ * Works in Node.js (18+) without NestJS or any framework.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { SmsTunnelNodeClient } from '@narcisbodea/smstunnel-sdk/node';
10
+ *
11
+ * const client = new SmsTunnelNodeClient({
12
+ * apiKey: 'sk_live_xxx',
13
+ * serverUrl: 'https://smstunnel.io',
14
+ * });
15
+ *
16
+ * // Send SMS
17
+ * const result = await client.sendSms('+40721123456', 'Hello!');
18
+ *
19
+ * // E2E: check status, get public key, send encrypted
20
+ * const e2e = await client.getDeviceE2EStatus('device-id');
21
+ * ```
22
+ */
23
+ interface SmsTunnelNodeClientOptions {
24
+ /** SMSTunnel API key (sk_live_...) */
25
+ apiKey: string;
26
+ /** SMSTunnel server URL (default: https://smstunnel.io) */
27
+ serverUrl?: string;
28
+ /** Request timeout in ms (default: 30000) */
29
+ timeout?: number;
30
+ }
31
+ interface SendSmsResponse {
32
+ success: boolean;
33
+ messageId?: string;
34
+ queued?: boolean;
35
+ data?: {
36
+ messageId: string;
37
+ branded: boolean;
38
+ queued?: boolean;
39
+ remaining: {
40
+ clean: number;
41
+ total: number | string;
42
+ };
43
+ };
44
+ error?: string;
45
+ }
46
+ interface SendBulkSmsResponse {
47
+ success: boolean;
48
+ data?: {
49
+ messageIds: string[];
50
+ branded: boolean;
51
+ count: number;
52
+ remaining: {
53
+ clean: number;
54
+ total: number | string;
55
+ };
56
+ };
57
+ error?: string;
58
+ }
59
+ interface MessageStatusResponse {
60
+ success: boolean;
61
+ data?: {
62
+ id: string;
63
+ phoneNumber: string;
64
+ status: 'pending' | 'sent' | 'delivered' | 'failed';
65
+ sentAt?: string;
66
+ deliveredAt?: string;
67
+ errorMessage?: string | null;
68
+ };
69
+ error?: string;
70
+ }
71
+ interface DevicesResponse {
72
+ success: boolean;
73
+ data?: {
74
+ devices: Array<{
75
+ id: string;
76
+ name: string;
77
+ isOnline: boolean;
78
+ lastSeen?: string;
79
+ }>;
80
+ online: number;
81
+ total: number;
82
+ };
83
+ error?: string;
84
+ }
85
+ interface AccountUsageResponse {
86
+ success: boolean;
87
+ data?: Record<string, any>;
88
+ error?: string;
89
+ }
90
+ interface ReceivedSmsResponse {
91
+ success: boolean;
92
+ data?: {
93
+ messages: Array<{
94
+ id: string;
95
+ from: string;
96
+ body: string;
97
+ receivedAt: string;
98
+ deviceId: string;
99
+ }>;
100
+ total: number;
101
+ page: number;
102
+ };
103
+ error?: string;
104
+ }
105
+ interface DeviceE2EStatus {
106
+ encryptionEnabled: boolean;
107
+ hasPublicKey: boolean;
108
+ publicKeyFingerprint?: string;
109
+ keyCreatedAt?: string;
110
+ }
111
+ interface DevicePublicKeyInfo {
112
+ deviceId: string;
113
+ deviceName: string;
114
+ publicKey: string;
115
+ }
116
+ interface VerifyDeviceKeyResult {
117
+ valid: boolean;
118
+ currentFingerprint?: string;
119
+ needsRePairing: boolean;
120
+ }
121
+ interface E2EPairingResponse {
122
+ success: boolean;
123
+ data?: {
124
+ token: string;
125
+ qrData: string;
126
+ expiresAt: string;
127
+ };
128
+ error?: string;
129
+ }
130
+ interface E2EPairingStatusResponse {
131
+ success: boolean;
132
+ data?: {
133
+ status: 'pending' | 'completed' | 'expired';
134
+ publicKey?: string;
135
+ publicKeyFingerprint?: string;
136
+ deviceId?: string;
137
+ deviceName?: string;
138
+ };
139
+ error?: string;
140
+ }
141
+ interface PairingTokenStatusResponse {
142
+ status: 'pending' | 'completed' | 'expired';
143
+ source?: string;
144
+ displayName?: string;
145
+ pairedDeviceId?: string;
146
+ pairingId?: string;
147
+ }
148
+ declare class SmsTunnelNodeClient {
149
+ private readonly apiKey;
150
+ private readonly serverUrl;
151
+ private readonly timeout;
152
+ constructor(options: SmsTunnelNodeClientOptions);
153
+ /** Send a single SMS */
154
+ sendSms(to: string, message: string, deviceId?: string): Promise<SendSmsResponse>;
155
+ /** Send 2FA SMS */
156
+ send2FaSms(to: string, message: string): Promise<SendSmsResponse>;
157
+ /** Send bulk SMS */
158
+ sendBulkSms(to: string[], message: string, deviceId?: string): Promise<SendBulkSmsResponse>;
159
+ /** Get message delivery status */
160
+ getMessageStatus(messageId: string): Promise<MessageStatusResponse>;
161
+ /** Get received SMS (inbox) */
162
+ getReceivedSms(params?: {
163
+ limit?: number;
164
+ page?: number;
165
+ since?: string;
166
+ phone?: string;
167
+ deviceId?: string;
168
+ }): Promise<ReceivedSmsResponse>;
169
+ /** List devices */
170
+ getDevices(): Promise<DevicesResponse>;
171
+ /** Get account usage stats */
172
+ getUsage(): Promise<AccountUsageResponse>;
173
+ /** Get E2E encryption status for a device */
174
+ getDeviceE2EStatus(deviceId: string): Promise<{
175
+ success: boolean;
176
+ data: DeviceE2EStatus;
177
+ }>;
178
+ /** Get device public key for E2E encryption */
179
+ getDevicePublicKey(deviceId: string): Promise<{
180
+ success: boolean;
181
+ data: DevicePublicKeyInfo;
182
+ }>;
183
+ /**
184
+ * Verify that a stored fingerprint matches the current device key.
185
+ * If it doesn't match, the device was reinstalled and re-pairing is needed.
186
+ */
187
+ verifyDeviceKey(deviceId: string, storedFingerprint: string): Promise<VerifyDeviceKeyResult>;
188
+ /** Send encrypted SMS (payload already encrypted with device RSA public key) */
189
+ sendEncryptedSms(encryptedPayload: string, deviceId: string, options?: {
190
+ is2FA?: boolean;
191
+ expectedFingerprint?: string;
192
+ }): Promise<SendSmsResponse>;
193
+ /** Create E2E pairing request (generates QR code) */
194
+ createE2EPairing(params?: {
195
+ siteName?: string;
196
+ siteUrl?: string;
197
+ }): Promise<E2EPairingResponse>;
198
+ /** Get E2E pairing status (poll this after creating a pairing) */
199
+ getE2EPairingStatus(token: string): Promise<E2EPairingStatusResponse>;
200
+ /** Create pairing token (unified v2 flow) */
201
+ createPairingToken(params: {
202
+ source: 'dashboard' | 'wordpress' | 'enterprise' | 'saas' | 'api' | 'e2e';
203
+ displayName: string;
204
+ displayUrl?: string;
205
+ context?: Record<string, any>;
206
+ expiresInMinutes?: number;
207
+ }): Promise<any>;
208
+ /** Get pairing token status (for polling) */
209
+ getPairingTokenStatus(token: string): Promise<PairingTokenStatusResponse>;
210
+ /** List all active pairings */
211
+ listPairings(): Promise<any[]>;
212
+ /** Unpair (revoke) a pairing */
213
+ unpair(pairingId: string): Promise<{
214
+ success: boolean;
215
+ }>;
216
+ /**
217
+ * Poll E2E pairing until completed, expired, or timeout.
218
+ * Returns the final status.
219
+ */
220
+ pollE2EPairing(token: string, options?: {
221
+ intervalMs?: number;
222
+ timeoutMs?: number;
223
+ }): Promise<E2EPairingStatusResponse>;
224
+ private get;
225
+ private post;
226
+ }
227
+
228
+ export { type AccountUsageResponse, type DeviceE2EStatus, type DevicePublicKeyInfo, type DevicesResponse, type E2EPairingResponse, type E2EPairingStatusResponse, type MessageStatusResponse, type PairingTokenStatusResponse, type ReceivedSmsResponse, type SendBulkSmsResponse, type SendSmsResponse, SmsTunnelNodeClient, type SmsTunnelNodeClientOptions, type VerifyDeviceKeyResult };
@@ -0,0 +1,208 @@
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/node/index.ts
21
+ var node_exports = {};
22
+ __export(node_exports, {
23
+ SmsTunnelNodeClient: () => SmsTunnelNodeClient
24
+ });
25
+ module.exports = __toCommonJS(node_exports);
26
+ var SmsTunnelNodeClient = class {
27
+ constructor(options) {
28
+ if (!options.apiKey) {
29
+ throw new Error("API key is required");
30
+ }
31
+ this.apiKey = options.apiKey;
32
+ this.serverUrl = (options.serverUrl || "https://smstunnel.io").replace(/\/$/, "");
33
+ this.timeout = options.timeout || 3e4;
34
+ }
35
+ // ─── SMS ────────────────────────────────────────────
36
+ /** Send a single SMS */
37
+ async sendSms(to, message, deviceId) {
38
+ return this.post("/api/v1/sms/send", { to, message, deviceId });
39
+ }
40
+ /** Send 2FA SMS */
41
+ async send2FaSms(to, message) {
42
+ return this.post("/api/v1/sms/send-2fa", { to, message });
43
+ }
44
+ /** Send bulk SMS */
45
+ async sendBulkSms(to, message, deviceId) {
46
+ return this.post("/api/v1/sms/send-bulk", { to, message, deviceId });
47
+ }
48
+ /** Get message delivery status */
49
+ async getMessageStatus(messageId) {
50
+ return this.get(`/api/v1/sms/status/${messageId}`);
51
+ }
52
+ /** Get received SMS (inbox) */
53
+ async getReceivedSms(params) {
54
+ const query = new URLSearchParams();
55
+ if (params?.limit) query.set("limit", String(params.limit));
56
+ if (params?.page) query.set("page", String(params.page));
57
+ if (params?.since) query.set("since", params.since);
58
+ if (params?.phone) query.set("phone", params.phone);
59
+ if (params?.deviceId) query.set("deviceId", params.deviceId);
60
+ const qs = query.toString();
61
+ return this.get(`/api/v1/sms/received${qs ? "?" + qs : ""}`);
62
+ }
63
+ // ─── Devices ────────────────────────────────────────
64
+ /** List devices */
65
+ async getDevices() {
66
+ return this.get("/api/v1/devices");
67
+ }
68
+ /** Get account usage stats */
69
+ async getUsage() {
70
+ return this.get("/api/v1/account/usage");
71
+ }
72
+ // ─── E2E Encryption ────────────────────────────────
73
+ /** Get E2E encryption status for a device */
74
+ async getDeviceE2EStatus(deviceId) {
75
+ return this.get(`/api/v1/devices/${encodeURIComponent(deviceId)}/e2e-status`);
76
+ }
77
+ /** Get device public key for E2E encryption */
78
+ async getDevicePublicKey(deviceId) {
79
+ return this.get(`/api/v1/devices/${encodeURIComponent(deviceId)}/public-key`);
80
+ }
81
+ /**
82
+ * Verify that a stored fingerprint matches the current device key.
83
+ * If it doesn't match, the device was reinstalled and re-pairing is needed.
84
+ */
85
+ async verifyDeviceKey(deviceId, storedFingerprint) {
86
+ const res = await this.getDeviceE2EStatus(deviceId);
87
+ const status = res.data;
88
+ if (!status?.hasPublicKey || !status?.publicKeyFingerprint) {
89
+ return { valid: false, needsRePairing: true };
90
+ }
91
+ const matches = status.publicKeyFingerprint === storedFingerprint;
92
+ return {
93
+ valid: matches,
94
+ currentFingerprint: status.publicKeyFingerprint,
95
+ needsRePairing: !matches
96
+ };
97
+ }
98
+ /** Send encrypted SMS (payload already encrypted with device RSA public key) */
99
+ async sendEncryptedSms(encryptedPayload, deviceId, options) {
100
+ if (options?.expectedFingerprint) {
101
+ const verification = await this.verifyDeviceKey(deviceId, options.expectedFingerprint);
102
+ if (verification.needsRePairing) {
103
+ return {
104
+ success: false,
105
+ error: "Device key changed since last pairing, re-pairing needed"
106
+ };
107
+ }
108
+ }
109
+ return this.post("/api/v1/sms/send", {
110
+ encrypted: true,
111
+ encryptedPayload,
112
+ deviceId,
113
+ is2FA: options?.is2FA || false
114
+ });
115
+ }
116
+ // ─── E2E Pairing ───────────────────────────────────
117
+ /** Create E2E pairing request (generates QR code) */
118
+ async createE2EPairing(params) {
119
+ return this.post("/api/v1/e2e/pair", params || {});
120
+ }
121
+ /** Get E2E pairing status (poll this after creating a pairing) */
122
+ async getE2EPairingStatus(token) {
123
+ return this.get(`/api/v1/e2e/pair/${encodeURIComponent(token)}`);
124
+ }
125
+ // ─── Unified Pairing v2 ────────────────────────────
126
+ /** Create pairing token (unified v2 flow) */
127
+ async createPairingToken(params) {
128
+ return this.post("/api/v1/pairing/create", params);
129
+ }
130
+ /** Get pairing token status (for polling) */
131
+ async getPairingTokenStatus(token) {
132
+ return this.get(`/api/v1/pairing/${encodeURIComponent(token)}`);
133
+ }
134
+ /** List all active pairings */
135
+ async listPairings() {
136
+ return this.get("/api/v1/pairings");
137
+ }
138
+ /** Unpair (revoke) a pairing */
139
+ async unpair(pairingId) {
140
+ return this.post(`/api/v1/unpair/${encodeURIComponent(pairingId)}`, {});
141
+ }
142
+ // ─── Helpers ────────────────────────────────────────
143
+ /**
144
+ * Poll E2E pairing until completed, expired, or timeout.
145
+ * Returns the final status.
146
+ */
147
+ async pollE2EPairing(token, options) {
148
+ const interval = options?.intervalMs || 2e3;
149
+ const timeout = options?.timeoutMs || 3e5;
150
+ const start = Date.now();
151
+ while (Date.now() - start < timeout) {
152
+ const status = await this.getE2EPairingStatus(token);
153
+ if (status.data?.status !== "pending") {
154
+ return status;
155
+ }
156
+ await new Promise((resolve) => setTimeout(resolve, interval));
157
+ }
158
+ return { success: true, data: { status: "expired" } };
159
+ }
160
+ // ─── Internal ──────────────────────────────────────
161
+ async get(path) {
162
+ const controller = new AbortController();
163
+ const timer = setTimeout(() => controller.abort(), this.timeout);
164
+ try {
165
+ const res = await fetch(`${this.serverUrl}${path}`, {
166
+ headers: {
167
+ "X-API-Key": this.apiKey,
168
+ "Content-Type": "application/json"
169
+ },
170
+ signal: controller.signal
171
+ });
172
+ const data = await res.json();
173
+ if (!res.ok) {
174
+ throw new Error(data?.message || data?.error || `HTTP ${res.status}`);
175
+ }
176
+ return data;
177
+ } finally {
178
+ clearTimeout(timer);
179
+ }
180
+ }
181
+ async post(path, body) {
182
+ const controller = new AbortController();
183
+ const timer = setTimeout(() => controller.abort(), this.timeout);
184
+ try {
185
+ const res = await fetch(`${this.serverUrl}${path}`, {
186
+ method: "POST",
187
+ headers: {
188
+ "X-API-Key": this.apiKey,
189
+ "Content-Type": "application/json"
190
+ },
191
+ body: JSON.stringify(body),
192
+ signal: controller.signal
193
+ });
194
+ const data = await res.json();
195
+ if (!res.ok) {
196
+ throw new Error(data?.message || data?.error || `HTTP ${res.status}`);
197
+ }
198
+ return data;
199
+ } finally {
200
+ clearTimeout(timer);
201
+ }
202
+ }
203
+ };
204
+ // Annotate the CommonJS export names for ESM import in node:
205
+ 0 && (module.exports = {
206
+ SmsTunnelNodeClient
207
+ });
208
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/node/index.ts"],"sourcesContent":["/**\r\n * SMSTunnel Node.js Client\r\n *\r\n * Standalone client that connects directly to the SMSTunnel API.\r\n * Works in Node.js (18+) without NestJS or any framework.\r\n *\r\n * @example\r\n * ```typescript\r\n * import { SmsTunnelNodeClient } from '@narcisbodea/smstunnel-sdk/node';\r\n *\r\n * const client = new SmsTunnelNodeClient({\r\n * apiKey: 'sk_live_xxx',\r\n * serverUrl: 'https://smstunnel.io',\r\n * });\r\n *\r\n * // Send SMS\r\n * const result = await client.sendSms('+40721123456', 'Hello!');\r\n *\r\n * // E2E: check status, get public key, send encrypted\r\n * const e2e = await client.getDeviceE2EStatus('device-id');\r\n * ```\r\n */\r\n\r\n// ─── Options ────────────────────────────────────────\r\n\r\nexport interface SmsTunnelNodeClientOptions {\r\n /** SMSTunnel API key (sk_live_...) */\r\n apiKey: string;\r\n /** SMSTunnel server URL (default: https://smstunnel.io) */\r\n serverUrl?: string;\r\n /** Request timeout in ms (default: 30000) */\r\n timeout?: number;\r\n}\r\n\r\n// ─── Response types ─────────────────────────────────\r\n\r\nexport interface SendSmsResponse {\r\n success: boolean;\r\n messageId?: string;\r\n queued?: boolean;\r\n data?: {\r\n messageId: string;\r\n branded: boolean;\r\n queued?: boolean;\r\n remaining: { clean: number; total: number | string };\r\n };\r\n error?: string;\r\n}\r\n\r\nexport interface SendBulkSmsResponse {\r\n success: boolean;\r\n data?: {\r\n messageIds: string[];\r\n branded: boolean;\r\n count: number;\r\n remaining: { clean: number; total: number | string };\r\n };\r\n error?: string;\r\n}\r\n\r\nexport interface MessageStatusResponse {\r\n success: boolean;\r\n data?: {\r\n id: string;\r\n phoneNumber: string;\r\n status: 'pending' | 'sent' | 'delivered' | 'failed';\r\n sentAt?: string;\r\n deliveredAt?: string;\r\n errorMessage?: string | null;\r\n };\r\n error?: string;\r\n}\r\n\r\nexport interface DevicesResponse {\r\n success: boolean;\r\n data?: {\r\n devices: Array<{\r\n id: string;\r\n name: string;\r\n isOnline: boolean;\r\n lastSeen?: string;\r\n }>;\r\n online: number;\r\n total: number;\r\n };\r\n error?: string;\r\n}\r\n\r\nexport interface AccountUsageResponse {\r\n success: boolean;\r\n data?: Record<string, any>;\r\n error?: string;\r\n}\r\n\r\nexport interface ReceivedSmsResponse {\r\n success: boolean;\r\n data?: {\r\n messages: Array<{\r\n id: string;\r\n from: string;\r\n body: string;\r\n receivedAt: string;\r\n deviceId: string;\r\n }>;\r\n total: number;\r\n page: number;\r\n };\r\n error?: string;\r\n}\r\n\r\n// ─── E2E types ──────────────────────────────────────\r\n\r\nexport interface DeviceE2EStatus {\r\n encryptionEnabled: boolean;\r\n hasPublicKey: boolean;\r\n publicKeyFingerprint?: string;\r\n keyCreatedAt?: string;\r\n}\r\n\r\nexport interface DevicePublicKeyInfo {\r\n deviceId: string;\r\n deviceName: string;\r\n publicKey: string;\r\n}\r\n\r\nexport interface VerifyDeviceKeyResult {\r\n valid: boolean;\r\n currentFingerprint?: string;\r\n needsRePairing: boolean;\r\n}\r\n\r\n// ─── Pairing types ──────────────────────────────────\r\n\r\nexport interface E2EPairingResponse {\r\n success: boolean;\r\n data?: {\r\n token: string;\r\n qrData: string;\r\n expiresAt: string;\r\n };\r\n error?: string;\r\n}\r\n\r\nexport interface E2EPairingStatusResponse {\r\n success: boolean;\r\n data?: {\r\n status: 'pending' | 'completed' | 'expired';\r\n publicKey?: string;\r\n publicKeyFingerprint?: string;\r\n deviceId?: string;\r\n deviceName?: string;\r\n };\r\n error?: string;\r\n}\r\n\r\nexport interface PairingTokenStatusResponse {\r\n status: 'pending' | 'completed' | 'expired';\r\n source?: string;\r\n displayName?: string;\r\n pairedDeviceId?: string;\r\n pairingId?: string;\r\n}\r\n\r\n// ─── Client ─────────────────────────────────────────\r\n\r\nexport class SmsTunnelNodeClient {\r\n private readonly apiKey: string;\r\n private readonly serverUrl: string;\r\n private readonly timeout: number;\r\n\r\n constructor(options: SmsTunnelNodeClientOptions) {\r\n if (!options.apiKey) {\r\n throw new Error('API key is required');\r\n }\r\n this.apiKey = options.apiKey;\r\n this.serverUrl = (options.serverUrl || 'https://smstunnel.io').replace(/\\/$/, '');\r\n this.timeout = options.timeout || 30000;\r\n }\r\n\r\n // ─── SMS ────────────────────────────────────────────\r\n\r\n /** Send a single SMS */\r\n async sendSms(to: string, message: string, deviceId?: string): Promise<SendSmsResponse> {\r\n return this.post('/api/v1/sms/send', { to, message, deviceId });\r\n }\r\n\r\n /** Send 2FA SMS */\r\n async send2FaSms(to: string, message: string): Promise<SendSmsResponse> {\r\n return this.post('/api/v1/sms/send-2fa', { to, message });\r\n }\r\n\r\n /** Send bulk SMS */\r\n async sendBulkSms(\r\n to: string[],\r\n message: string,\r\n deviceId?: string,\r\n ): Promise<SendBulkSmsResponse> {\r\n return this.post('/api/v1/sms/send-bulk', { to, message, deviceId });\r\n }\r\n\r\n /** Get message delivery status */\r\n async getMessageStatus(messageId: string): Promise<MessageStatusResponse> {\r\n return this.get(`/api/v1/sms/status/${messageId}`);\r\n }\r\n\r\n /** Get received SMS (inbox) */\r\n async getReceivedSms(params?: {\r\n limit?: number;\r\n page?: number;\r\n since?: string;\r\n phone?: string;\r\n deviceId?: string;\r\n }): Promise<ReceivedSmsResponse> {\r\n const query = new URLSearchParams();\r\n if (params?.limit) query.set('limit', String(params.limit));\r\n if (params?.page) query.set('page', String(params.page));\r\n if (params?.since) query.set('since', params.since);\r\n if (params?.phone) query.set('phone', params.phone);\r\n if (params?.deviceId) query.set('deviceId', params.deviceId);\r\n const qs = query.toString();\r\n return this.get(`/api/v1/sms/received${qs ? '?' + qs : ''}`);\r\n }\r\n\r\n // ─── Devices ────────────────────────────────────────\r\n\r\n /** List devices */\r\n async getDevices(): Promise<DevicesResponse> {\r\n return this.get('/api/v1/devices');\r\n }\r\n\r\n /** Get account usage stats */\r\n async getUsage(): Promise<AccountUsageResponse> {\r\n return this.get('/api/v1/account/usage');\r\n }\r\n\r\n // ─── E2E Encryption ────────────────────────────────\r\n\r\n /** Get E2E encryption status for a device */\r\n async getDeviceE2EStatus(deviceId: string): Promise<{ success: boolean; data: DeviceE2EStatus }> {\r\n return this.get(`/api/v1/devices/${encodeURIComponent(deviceId)}/e2e-status`);\r\n }\r\n\r\n /** Get device public key for E2E encryption */\r\n async getDevicePublicKey(deviceId: string): Promise<{ success: boolean; data: DevicePublicKeyInfo }> {\r\n return this.get(`/api/v1/devices/${encodeURIComponent(deviceId)}/public-key`);\r\n }\r\n\r\n /**\r\n * Verify that a stored fingerprint matches the current device key.\r\n * If it doesn't match, the device was reinstalled and re-pairing is needed.\r\n */\r\n async verifyDeviceKey(deviceId: string, storedFingerprint: string): Promise<VerifyDeviceKeyResult> {\r\n const res = await this.getDeviceE2EStatus(deviceId);\r\n const status = res.data;\r\n if (!status?.hasPublicKey || !status?.publicKeyFingerprint) {\r\n return { valid: false, needsRePairing: true };\r\n }\r\n const matches = status.publicKeyFingerprint === storedFingerprint;\r\n return {\r\n valid: matches,\r\n currentFingerprint: status.publicKeyFingerprint,\r\n needsRePairing: !matches,\r\n };\r\n }\r\n\r\n /** Send encrypted SMS (payload already encrypted with device RSA public key) */\r\n async sendEncryptedSms(\r\n encryptedPayload: string,\r\n deviceId: string,\r\n options?: { is2FA?: boolean; expectedFingerprint?: string },\r\n ): Promise<SendSmsResponse> {\r\n // Verify E2E status before sending\r\n if (options?.expectedFingerprint) {\r\n const verification = await this.verifyDeviceKey(deviceId, options.expectedFingerprint);\r\n if (verification.needsRePairing) {\r\n return {\r\n success: false,\r\n error: 'Device key changed since last pairing, re-pairing needed',\r\n };\r\n }\r\n }\r\n\r\n return this.post('/api/v1/sms/send', {\r\n encrypted: true,\r\n encryptedPayload,\r\n deviceId,\r\n is2FA: options?.is2FA || false,\r\n });\r\n }\r\n\r\n // ─── E2E Pairing ───────────────────────────────────\r\n\r\n /** Create E2E pairing request (generates QR code) */\r\n async createE2EPairing(params?: {\r\n siteName?: string;\r\n siteUrl?: string;\r\n }): Promise<E2EPairingResponse> {\r\n return this.post('/api/v1/e2e/pair', params || {});\r\n }\r\n\r\n /** Get E2E pairing status (poll this after creating a pairing) */\r\n async getE2EPairingStatus(token: string): Promise<E2EPairingStatusResponse> {\r\n return this.get(`/api/v1/e2e/pair/${encodeURIComponent(token)}`);\r\n }\r\n\r\n // ─── Unified Pairing v2 ────────────────────────────\r\n\r\n /** Create pairing token (unified v2 flow) */\r\n async createPairingToken(params: {\r\n source: 'dashboard' | 'wordpress' | 'enterprise' | 'saas' | 'api' | 'e2e';\r\n displayName: string;\r\n displayUrl?: string;\r\n context?: Record<string, any>;\r\n expiresInMinutes?: number;\r\n }): Promise<any> {\r\n return this.post('/api/v1/pairing/create', params);\r\n }\r\n\r\n /** Get pairing token status (for polling) */\r\n async getPairingTokenStatus(token: string): Promise<PairingTokenStatusResponse> {\r\n return this.get(`/api/v1/pairing/${encodeURIComponent(token)}`);\r\n }\r\n\r\n /** List all active pairings */\r\n async listPairings(): Promise<any[]> {\r\n return this.get('/api/v1/pairings');\r\n }\r\n\r\n /** Unpair (revoke) a pairing */\r\n async unpair(pairingId: string): Promise<{ success: boolean }> {\r\n return this.post(`/api/v1/unpair/${encodeURIComponent(pairingId)}`, {});\r\n }\r\n\r\n // ─── Helpers ────────────────────────────────────────\r\n\r\n /**\r\n * Poll E2E pairing until completed, expired, or timeout.\r\n * Returns the final status.\r\n */\r\n async pollE2EPairing(\r\n token: string,\r\n options?: { intervalMs?: number; timeoutMs?: number },\r\n ): Promise<E2EPairingStatusResponse> {\r\n const interval = options?.intervalMs || 2000;\r\n const timeout = options?.timeoutMs || 300000; // 5 min default\r\n const start = Date.now();\r\n\r\n while (Date.now() - start < timeout) {\r\n const status = await this.getE2EPairingStatus(token);\r\n if (status.data?.status !== 'pending') {\r\n return status;\r\n }\r\n await new Promise((resolve) => setTimeout(resolve, interval));\r\n }\r\n\r\n return { success: true, data: { status: 'expired' } };\r\n }\r\n\r\n // ─── Internal ──────────────────────────────────────\r\n\r\n private async get<T = any>(path: string): Promise<T> {\r\n const controller = new AbortController();\r\n const timer = setTimeout(() => controller.abort(), this.timeout);\r\n\r\n try {\r\n const res = await fetch(`${this.serverUrl}${path}`, {\r\n headers: {\r\n 'X-API-Key': this.apiKey,\r\n 'Content-Type': 'application/json',\r\n },\r\n signal: controller.signal,\r\n });\r\n\r\n const data = await res.json();\r\n if (!res.ok) {\r\n throw new Error(data?.message || data?.error || `HTTP ${res.status}`);\r\n }\r\n return data as T;\r\n } finally {\r\n clearTimeout(timer);\r\n }\r\n }\r\n\r\n private async post<T = any>(path: string, body: any): Promise<T> {\r\n const controller = new AbortController();\r\n const timer = setTimeout(() => controller.abort(), this.timeout);\r\n\r\n try {\r\n const res = await fetch(`${this.serverUrl}${path}`, {\r\n method: 'POST',\r\n headers: {\r\n 'X-API-Key': this.apiKey,\r\n 'Content-Type': 'application/json',\r\n },\r\n body: JSON.stringify(body),\r\n signal: controller.signal,\r\n });\r\n\r\n const data = await res.json();\r\n if (!res.ok) {\r\n throw new Error(data?.message || data?.error || `HTTP ${res.status}`);\r\n }\r\n return data as T;\r\n } finally {\r\n clearTimeout(timer);\r\n }\r\n }\r\n}\r\n"],"mappings":"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","names":[]}
@@ -0,0 +1,183 @@
1
+ // src/node/index.ts
2
+ var SmsTunnelNodeClient = class {
3
+ constructor(options) {
4
+ if (!options.apiKey) {
5
+ throw new Error("API key is required");
6
+ }
7
+ this.apiKey = options.apiKey;
8
+ this.serverUrl = (options.serverUrl || "https://smstunnel.io").replace(/\/$/, "");
9
+ this.timeout = options.timeout || 3e4;
10
+ }
11
+ // ─── SMS ────────────────────────────────────────────
12
+ /** Send a single SMS */
13
+ async sendSms(to, message, deviceId) {
14
+ return this.post("/api/v1/sms/send", { to, message, deviceId });
15
+ }
16
+ /** Send 2FA SMS */
17
+ async send2FaSms(to, message) {
18
+ return this.post("/api/v1/sms/send-2fa", { to, message });
19
+ }
20
+ /** Send bulk SMS */
21
+ async sendBulkSms(to, message, deviceId) {
22
+ return this.post("/api/v1/sms/send-bulk", { to, message, deviceId });
23
+ }
24
+ /** Get message delivery status */
25
+ async getMessageStatus(messageId) {
26
+ return this.get(`/api/v1/sms/status/${messageId}`);
27
+ }
28
+ /** Get received SMS (inbox) */
29
+ async getReceivedSms(params) {
30
+ const query = new URLSearchParams();
31
+ if (params?.limit) query.set("limit", String(params.limit));
32
+ if (params?.page) query.set("page", String(params.page));
33
+ if (params?.since) query.set("since", params.since);
34
+ if (params?.phone) query.set("phone", params.phone);
35
+ if (params?.deviceId) query.set("deviceId", params.deviceId);
36
+ const qs = query.toString();
37
+ return this.get(`/api/v1/sms/received${qs ? "?" + qs : ""}`);
38
+ }
39
+ // ─── Devices ────────────────────────────────────────
40
+ /** List devices */
41
+ async getDevices() {
42
+ return this.get("/api/v1/devices");
43
+ }
44
+ /** Get account usage stats */
45
+ async getUsage() {
46
+ return this.get("/api/v1/account/usage");
47
+ }
48
+ // ─── E2E Encryption ────────────────────────────────
49
+ /** Get E2E encryption status for a device */
50
+ async getDeviceE2EStatus(deviceId) {
51
+ return this.get(`/api/v1/devices/${encodeURIComponent(deviceId)}/e2e-status`);
52
+ }
53
+ /** Get device public key for E2E encryption */
54
+ async getDevicePublicKey(deviceId) {
55
+ return this.get(`/api/v1/devices/${encodeURIComponent(deviceId)}/public-key`);
56
+ }
57
+ /**
58
+ * Verify that a stored fingerprint matches the current device key.
59
+ * If it doesn't match, the device was reinstalled and re-pairing is needed.
60
+ */
61
+ async verifyDeviceKey(deviceId, storedFingerprint) {
62
+ const res = await this.getDeviceE2EStatus(deviceId);
63
+ const status = res.data;
64
+ if (!status?.hasPublicKey || !status?.publicKeyFingerprint) {
65
+ return { valid: false, needsRePairing: true };
66
+ }
67
+ const matches = status.publicKeyFingerprint === storedFingerprint;
68
+ return {
69
+ valid: matches,
70
+ currentFingerprint: status.publicKeyFingerprint,
71
+ needsRePairing: !matches
72
+ };
73
+ }
74
+ /** Send encrypted SMS (payload already encrypted with device RSA public key) */
75
+ async sendEncryptedSms(encryptedPayload, deviceId, options) {
76
+ if (options?.expectedFingerprint) {
77
+ const verification = await this.verifyDeviceKey(deviceId, options.expectedFingerprint);
78
+ if (verification.needsRePairing) {
79
+ return {
80
+ success: false,
81
+ error: "Device key changed since last pairing, re-pairing needed"
82
+ };
83
+ }
84
+ }
85
+ return this.post("/api/v1/sms/send", {
86
+ encrypted: true,
87
+ encryptedPayload,
88
+ deviceId,
89
+ is2FA: options?.is2FA || false
90
+ });
91
+ }
92
+ // ─── E2E Pairing ───────────────────────────────────
93
+ /** Create E2E pairing request (generates QR code) */
94
+ async createE2EPairing(params) {
95
+ return this.post("/api/v1/e2e/pair", params || {});
96
+ }
97
+ /** Get E2E pairing status (poll this after creating a pairing) */
98
+ async getE2EPairingStatus(token) {
99
+ return this.get(`/api/v1/e2e/pair/${encodeURIComponent(token)}`);
100
+ }
101
+ // ─── Unified Pairing v2 ────────────────────────────
102
+ /** Create pairing token (unified v2 flow) */
103
+ async createPairingToken(params) {
104
+ return this.post("/api/v1/pairing/create", params);
105
+ }
106
+ /** Get pairing token status (for polling) */
107
+ async getPairingTokenStatus(token) {
108
+ return this.get(`/api/v1/pairing/${encodeURIComponent(token)}`);
109
+ }
110
+ /** List all active pairings */
111
+ async listPairings() {
112
+ return this.get("/api/v1/pairings");
113
+ }
114
+ /** Unpair (revoke) a pairing */
115
+ async unpair(pairingId) {
116
+ return this.post(`/api/v1/unpair/${encodeURIComponent(pairingId)}`, {});
117
+ }
118
+ // ─── Helpers ────────────────────────────────────────
119
+ /**
120
+ * Poll E2E pairing until completed, expired, or timeout.
121
+ * Returns the final status.
122
+ */
123
+ async pollE2EPairing(token, options) {
124
+ const interval = options?.intervalMs || 2e3;
125
+ const timeout = options?.timeoutMs || 3e5;
126
+ const start = Date.now();
127
+ while (Date.now() - start < timeout) {
128
+ const status = await this.getE2EPairingStatus(token);
129
+ if (status.data?.status !== "pending") {
130
+ return status;
131
+ }
132
+ await new Promise((resolve) => setTimeout(resolve, interval));
133
+ }
134
+ return { success: true, data: { status: "expired" } };
135
+ }
136
+ // ─── Internal ──────────────────────────────────────
137
+ async get(path) {
138
+ const controller = new AbortController();
139
+ const timer = setTimeout(() => controller.abort(), this.timeout);
140
+ try {
141
+ const res = await fetch(`${this.serverUrl}${path}`, {
142
+ headers: {
143
+ "X-API-Key": this.apiKey,
144
+ "Content-Type": "application/json"
145
+ },
146
+ signal: controller.signal
147
+ });
148
+ const data = await res.json();
149
+ if (!res.ok) {
150
+ throw new Error(data?.message || data?.error || `HTTP ${res.status}`);
151
+ }
152
+ return data;
153
+ } finally {
154
+ clearTimeout(timer);
155
+ }
156
+ }
157
+ async post(path, body) {
158
+ const controller = new AbortController();
159
+ const timer = setTimeout(() => controller.abort(), this.timeout);
160
+ try {
161
+ const res = await fetch(`${this.serverUrl}${path}`, {
162
+ method: "POST",
163
+ headers: {
164
+ "X-API-Key": this.apiKey,
165
+ "Content-Type": "application/json"
166
+ },
167
+ body: JSON.stringify(body),
168
+ signal: controller.signal
169
+ });
170
+ const data = await res.json();
171
+ if (!res.ok) {
172
+ throw new Error(data?.message || data?.error || `HTTP ${res.status}`);
173
+ }
174
+ return data;
175
+ } finally {
176
+ clearTimeout(timer);
177
+ }
178
+ }
179
+ };
180
+ export {
181
+ SmsTunnelNodeClient
182
+ };
183
+ //# sourceMappingURL=index.mjs.map