@narcisbodea/smstunnel-sdk 1.1.2 → 1.1.4

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.
Files changed (2) hide show
  1. package/README.md +182 -63
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -1,18 +1,74 @@
1
- # smstunnel-sdk
1
+ # @narcisbodea/smstunnel-sdk
2
2
 
3
- SMSTunnel SDK - NestJS + React + Vue + framework-agnostic client for SMS pairing.
3
+ SMSTunnel SDK - Node.js + NestJS + React + Vue + Angular + framework-agnostic client for SMS pairing & E2E encryption.
4
4
 
5
5
  | Import path | Scope |
6
6
  |---|---|
7
- | `smstunnel-sdk` | `SmsTunnelClient` class + types + labels (Angular, Svelte, Astro, Solid, Vanilla JS) |
8
- | `smstunnel-sdk/server` | NestJS module (backend) |
9
- | `smstunnel-sdk/react` | React components + hook (also Next.js, Remix, Gatsby) |
10
- | `smstunnel-sdk/vue` | Vue 3 components + composable (also Nuxt) |
7
+ | `@narcisbodea/smstunnel-sdk` | `SmsTunnelClient` class + types + labels (Svelte, Astro, Solid, Vanilla JS) |
8
+ | `@narcisbodea/smstunnel-sdk/server` | NestJS module (backend proxy) |
9
+ | `@narcisbodea/smstunnel-sdk/react` | React components + hook (Next.js, Remix, Gatsby) |
10
+ | `@narcisbodea/smstunnel-sdk/vue` | Vue 3 components + composable (Nuxt) |
11
+ | `@narcisbodea/smstunnel-sdk/angular` | Angular 17+ injectable service |
12
+ | `@narcisbodea/smstunnel-sdk/node` | Standalone Node.js client (no framework, API key auth) |
11
13
 
12
14
  ## Install
13
15
 
14
16
  ```bash
15
- npm install smstunnel-sdk
17
+ npm install @narcisbodea/smstunnel-sdk
18
+ ```
19
+
20
+ ---
21
+
22
+ ## Node.js (Standalone)
23
+
24
+ Works without NestJS or any framework. Uses native `fetch` (Node 18+), zero external dependencies.
25
+
26
+ ```typescript
27
+ import { SmsTunnelNodeClient } from '@narcisbodea/smstunnel-sdk/node';
28
+
29
+ const client = new SmsTunnelNodeClient({
30
+ apiKey: 'sk_live_xxx',
31
+ serverUrl: 'https://smstunnel.io',
32
+ });
33
+
34
+ // Send SMS
35
+ const result = await client.sendSms('+40721123456', 'Hello!');
36
+
37
+ // Send 2FA
38
+ await client.send2FaSms('+40721123456', 'Your code is 123456');
39
+
40
+ // Send bulk SMS
41
+ await client.sendBulkSms(['+40721111111', '+40722222222'], 'Bulk message');
42
+
43
+ // Get message status
44
+ const status = await client.getMessageStatus('msg-id');
45
+
46
+ // Get received SMS (inbox)
47
+ const inbox = await client.getReceivedSms({ limit: 20, page: 1 });
48
+
49
+ // List devices
50
+ const devices = await client.getDevices();
51
+
52
+ // E2E Encryption
53
+ const e2eStatus = await client.getDeviceE2EStatus('device-id');
54
+ const publicKey = await client.getDevicePublicKey('device-id');
55
+ const verification = await client.verifyDeviceKey('device-id', 'stored-fingerprint');
56
+ await client.sendEncryptedSms('encrypted-payload', 'device-id', {
57
+ is2FA: false,
58
+ expectedFingerprint: 'a1b2c3d4',
59
+ });
60
+
61
+ // Unified Pairing v2
62
+ const pairing = await client.createPairingToken({
63
+ source: 'api',
64
+ displayName: 'My App',
65
+ });
66
+ const pairingStatus = await client.getPairingTokenStatus(pairing.data.token);
67
+
68
+ // E2E Pairing (with public key exchange)
69
+ const e2ePairing = await client.createE2EPairing({ siteName: 'My App' });
70
+ const e2eResult = await client.pollE2EPairing(e2ePairing.data.token);
71
+ // e2eResult.data.publicKey contains the device's RSA public key
16
72
  ```
17
73
 
18
74
  ---
@@ -26,7 +82,7 @@ The SDK is database-agnostic. You provide a simple adapter with 3 methods:
26
82
  #### MongoDB (Mongoose)
27
83
 
28
84
  ```typescript
29
- import { SmsTunnelStorageAdapter, SmsTunnelConfig } from 'smstunnel-sdk/server';
85
+ import { SmsTunnelStorageAdapter, SmsTunnelConfig } from '@narcisbodea/smstunnel-sdk/server';
30
86
  import { Model } from 'mongoose';
31
87
 
32
88
  export class MongoSmsTunnelStorage implements SmsTunnelStorageAdapter {
@@ -62,7 +118,7 @@ export class MongoSmsTunnelStorage implements SmsTunnelStorageAdapter {
62
118
  #### Prisma (PostgreSQL)
63
119
 
64
120
  ```typescript
65
- import { SmsTunnelStorageAdapter, SmsTunnelConfig } from 'smstunnel-sdk/server';
121
+ import { SmsTunnelStorageAdapter, SmsTunnelConfig } from '@narcisbodea/smstunnel-sdk/server';
66
122
  import { PrismaClient } from '@prisma/client';
67
123
 
68
124
  export class PrismaSmsTunnelStorage implements SmsTunnelStorageAdapter {
@@ -93,7 +149,7 @@ export class PrismaSmsTunnelStorage implements SmsTunnelStorageAdapter {
93
149
  #### JSON File
94
150
 
95
151
  ```typescript
96
- import { SmsTunnelStorageAdapter, SmsTunnelConfig } from 'smstunnel-sdk/server';
152
+ import { SmsTunnelStorageAdapter, SmsTunnelConfig } from '@narcisbodea/smstunnel-sdk/server';
97
153
  import { readFile, writeFile } from 'fs/promises';
98
154
 
99
155
  export class JsonFileStorage implements SmsTunnelStorageAdapter {
@@ -118,7 +174,7 @@ export class JsonFileStorage implements SmsTunnelStorageAdapter {
118
174
  ### 2. Register the Module
119
175
 
120
176
  ```typescript
121
- import { SmsTunnelModule } from 'smstunnel-sdk/server';
177
+ import { SmsTunnelModule } from '@narcisbodea/smstunnel-sdk/server';
122
178
 
123
179
  @Module({
124
180
  imports: [
@@ -147,7 +203,7 @@ SmsTunnelModule.forRootAsync({
147
203
 
148
204
  ### Multi-Tenant (Enterprise/SaaS) Mode
149
205
 
150
- For **multi-tenant applications** (SaaS platforms, CRMs, ERPs) where each tenant needs their own SMS device, configure the Enterprise API Key. The SDK will automatically provision sub-accounts for each tenant under your Enterprise umbrella:
206
+ For **multi-tenant applications** (SaaS platforms, CRMs, ERPs) where each tenant needs their own SMS device:
151
207
 
152
208
  ```typescript
153
209
  SmsTunnelModule.forRootAsync({
@@ -155,7 +211,6 @@ SmsTunnelModule.forRootAsync({
155
211
  storage: new MongoSmsTunnelStorage(settingsModel),
156
212
  callbackBaseUrl: process.env.CALLBACK_URL,
157
213
  displayName: 'My SaaS App',
158
- // Multi-tenant mode
159
214
  enterpriseApiKey: process.env.SMSTUNNEL_ENTERPRISE_API_KEY,
160
215
  externalId: tenantService.getCurrentTenantId(),
161
216
  }),
@@ -163,12 +218,10 @@ SmsTunnelModule.forRootAsync({
163
218
  })
164
219
  ```
165
220
 
166
- When `enterpriseApiKey` is set, `createPairingToken()` automatically uses `POST /api/v1/saas/activate` to create isolated sub-accounts per tenant. The callback flow and SMS sending remain the same — the SDK handles the routing transparently.
167
-
168
221
  ### 3. Auth Guard Integration
169
222
 
170
223
  ```typescript
171
- import { SMSTUNNEL_PUBLIC_PATHS } from 'smstunnel-sdk/server';
224
+ import { SMSTUNNEL_PUBLIC_PATHS } from '@narcisbodea/smstunnel-sdk/server';
172
225
 
173
226
  // In your JwtAuthGuard:
174
227
  if (SMSTUNNEL_PUBLIC_PATHS.some(p => request.url.includes(p))) return true;
@@ -185,6 +238,11 @@ if (SMSTUNNEL_PUBLIC_PATHS.some(p => request.url.includes(p))) return true;
185
238
  | `POST /smstunnel/unpair` | unpair | Yes | Disconnect device |
186
239
  | `POST /smstunnel/send` | sendSms | Yes | Send SMS |
187
240
  | `POST /smstunnel/update-config` | updateConfig | Yes | Set server URL |
241
+ | `GET /smstunnel/e2e-status/:deviceId` | getDeviceE2EStatus | Yes | E2E encryption status |
242
+ | `GET /smstunnel/public-key/:deviceId` | getDevicePublicKey | Yes | Get device public key |
243
+ | `POST /smstunnel/verify-key` | verifyDeviceKey | Yes | Verify key fingerprint |
244
+ | `POST /smstunnel/send-encrypted` | sendEncryptedSms | Yes | Send encrypted SMS |
245
+ | `GET /smstunnel/pairings` | getPairings | Yes | List active pairings |
188
246
 
189
247
  ---
190
248
 
@@ -195,7 +253,7 @@ Works with: **React, Next.js, Remix, Gatsby**
195
253
  ### Plug and Play Component
196
254
 
197
255
  ```tsx
198
- import { SmsTunnelPairing, RO_LABELS } from 'smstunnel-sdk/react';
256
+ import { SmsTunnelPairing, RO_LABELS } from '@narcisbodea/smstunnel-sdk/react';
199
257
 
200
258
  function SettingsPage() {
201
259
  return (
@@ -220,7 +278,7 @@ Add `'use client'` in your page/component:
220
278
 
221
279
  ```tsx
222
280
  'use client';
223
- import { SmsTunnelPairing, RO_LABELS } from 'smstunnel-sdk/react';
281
+ import { SmsTunnelPairing, RO_LABELS } from '@narcisbodea/smstunnel-sdk/react';
224
282
 
225
283
  export default function SmsSettings() {
226
284
  return <SmsTunnelPairing apiBaseUrl="/api" labels={RO_LABELS} />;
@@ -230,7 +288,7 @@ export default function SmsSettings() {
230
288
  ### Custom UI with Hook
231
289
 
232
290
  ```tsx
233
- import { useSmsTunnel, QrCodeCanvas } from 'smstunnel-sdk/react';
291
+ import { useSmsTunnel, QrCodeCanvas } from '@narcisbodea/smstunnel-sdk/react';
234
292
 
235
293
  function MySmsSettings() {
236
294
  const tunnel = useSmsTunnel({
@@ -238,6 +296,12 @@ function MySmsSettings() {
238
296
  getAuthHeaders: () => ({ Authorization: `Bearer ${token}` }),
239
297
  });
240
298
 
299
+ // E2E Encryption
300
+ const e2eStatus = await tunnel.getDeviceE2EStatus('device-id');
301
+ const publicKey = await tunnel.getDevicePublicKey('device-id');
302
+ const keyValid = await tunnel.verifyDeviceKey('device-id', 'fingerprint');
303
+ await tunnel.sendEncryptedSms('payload', 'device-id');
304
+
241
305
  if (tunnel.status === 'paired') {
242
306
  return <div>Connected: {tunnel.deviceName}</div>;
243
307
  }
@@ -269,7 +333,7 @@ Works with: **Vue 3, Nuxt 3**
269
333
 
270
334
  ```vue
271
335
  <script setup>
272
- import { SmsTunnelPairing, RO_LABELS } from 'smstunnel-sdk/vue';
336
+ import { SmsTunnelPairing, RO_LABELS } from '@narcisbodea/smstunnel-sdk/vue';
273
337
 
274
338
  function onPaired(device) {
275
339
  console.log('Paired:', device);
@@ -299,7 +363,7 @@ Wrap in `<ClientOnly>` since pairing uses browser APIs:
299
363
  </template>
300
364
 
301
365
  <script setup>
302
- import { SmsTunnelPairing, RO_LABELS } from 'smstunnel-sdk/vue';
366
+ import { SmsTunnelPairing, RO_LABELS } from '@narcisbodea/smstunnel-sdk/vue';
303
367
  </script>
304
368
  ```
305
369
 
@@ -307,12 +371,17 @@ import { SmsTunnelPairing, RO_LABELS } from 'smstunnel-sdk/vue';
307
371
 
308
372
  ```vue
309
373
  <script setup>
310
- import { useSmsTunnel, QrCodeCanvas } from 'smstunnel-sdk/vue';
374
+ import { useSmsTunnel, QrCodeCanvas } from '@narcisbodea/smstunnel-sdk/vue';
311
375
 
312
376
  const tunnel = useSmsTunnel({
313
377
  apiBaseUrl: '/api',
314
378
  getAuthHeaders: () => ({ Authorization: `Bearer ${token}` }),
315
379
  });
380
+
381
+ // E2E Encryption
382
+ const e2eStatus = await tunnel.getDeviceE2EStatus('device-id');
383
+ const publicKey = await tunnel.getDevicePublicKey('device-id');
384
+ await tunnel.sendEncryptedSms('payload', 'device-id');
316
385
  </script>
317
386
 
318
387
  <template>
@@ -336,46 +405,59 @@ const tunnel = useSmsTunnel({
336
405
 
337
406
  ## Angular
338
407
 
339
- Use the framework-agnostic `SmsTunnelClient` class:
408
+ Works with: **Angular 17+** (standalone)
340
409
 
341
- ```typescript
342
- import { Injectable, OnDestroy } from '@angular/core';
343
- import { SmsTunnelClient } from 'smstunnel-sdk';
410
+ ### Setup
344
411
 
345
- @Injectable({ providedIn: 'root' })
346
- export class SmsTunnelService implements OnDestroy {
347
- private client = new SmsTunnelClient({
348
- apiBaseUrl: '/api',
349
- getAuthHeaders: () => ({
350
- Authorization: `Bearer ${localStorage.getItem('token')}`,
412
+ ```typescript
413
+ // app.config.ts
414
+ import { provideSmsTunnel } from '@narcisbodea/smstunnel-sdk/angular';
415
+
416
+ export const appConfig: ApplicationConfig = {
417
+ providers: [
418
+ provideSmsTunnel({
419
+ apiBaseUrl: 'https://your-backend.com',
420
+ getAuthHeaders: () => ({
421
+ Authorization: `Bearer ${localStorage.getItem('token')}`,
422
+ }),
351
423
  }),
352
- });
353
-
354
- getStatus() { return this.client.getStatus(); }
355
- createToken() { return this.client.createToken(); }
356
- startPolling(token: string, cb: any) { return this.client.startPolling(token, cb); }
357
- sendSms(to: string, msg: string) { return this.client.sendSms(to, msg); }
358
- unpair() { return this.client.unpair(); }
359
- updateServerUrl(url: string) { return this.client.updateServerUrl(url); }
360
-
361
- ngOnDestroy() { this.client.destroy(); }
362
- }
424
+ ],
425
+ };
363
426
  ```
364
427
 
428
+ ### Usage
429
+
365
430
  ```typescript
366
- // Component
367
- @Component({ template: `...` })
431
+ import { Component, inject } from '@angular/core';
432
+ import { SmsTunnelService } from '@narcisbodea/smstunnel-sdk/angular';
433
+
434
+ @Component({ /* ... */ })
368
435
  export class SmsPairingComponent {
369
- constructor(private sms: SmsTunnelService) {}
436
+ private sms = inject(SmsTunnelService);
370
437
 
371
438
  async connect() {
372
- const result = await this.sms.createToken();
373
- if (result.success) {
374
- this.qrData = result.qrData;
375
- this.sms.startPolling(result.token!, (event) => {
376
- if (event === 'completed') this.loadStatus();
377
- });
378
- }
439
+ // Pairing
440
+ const status = await this.sms.getStatus();
441
+ const token = await this.sms.createToken();
442
+ const pairings = await this.sms.getPairings();
443
+ await this.sms.unpair();
444
+
445
+ // SMS
446
+ await this.sms.sendSms('+40721123456', 'Hello!');
447
+ await this.sms.send2fa('+40721123456', '123456');
448
+ await this.sms.sendBulk([{ to: '+40721111111', message: 'Hi' }]);
449
+ const msgStatus = await this.sms.getSmsStatus('msg-id');
450
+ const inbox = await this.sms.getReceivedSms();
451
+
452
+ // Devices
453
+ const devices = await this.sms.getDevices();
454
+ const usage = await this.sms.getUsage();
455
+
456
+ // E2E Encryption
457
+ const e2eStatus = await this.sms.getDeviceE2EStatus('device-id');
458
+ const publicKey = await this.sms.getDevicePublicKey('device-id');
459
+ const verification = await this.sms.verifyDeviceKey('device-id', 'fingerprint');
460
+ await this.sms.sendEncryptedSms('payload', 'device-id');
379
461
  }
380
462
  }
381
463
  ```
@@ -386,7 +468,7 @@ export class SmsPairingComponent {
386
468
 
387
469
  ```svelte
388
470
  <script>
389
- import { SmsTunnelClient } from 'smstunnel-sdk';
471
+ import { SmsTunnelClient } from '@narcisbodea/smstunnel-sdk';
390
472
  import { onMount, onDestroy } from 'svelte';
391
473
 
392
474
  let status = 'loading';
@@ -442,7 +524,7 @@ Astro supports React, Vue, and Svelte islands. Use any of the above:
442
524
  // pages/settings.astro
443
525
  ---
444
526
  <script>
445
- import { SmsTunnelPairing, RO_LABELS } from 'smstunnel-sdk/react';
527
+ import { SmsTunnelPairing, RO_LABELS } from '@narcisbodea/smstunnel-sdk/react';
446
528
  </script>
447
529
 
448
530
  <SmsTunnelPairing client:only="react" apiBaseUrl="/api" labels={RO_LABELS} />
@@ -456,10 +538,35 @@ Or with Vue:
456
538
 
457
539
  ---
458
540
 
541
+ ## E2E Encryption
542
+
543
+ All SDKs support end-to-end encryption using RSA-2048. The device holds the private key, the server stores the public key.
544
+
545
+ ### Flow
546
+
547
+ 1. Check if device has E2E enabled: `getDeviceE2EStatus(deviceId)`
548
+ 2. Get the device's public key: `getDevicePublicKey(deviceId)`
549
+ 3. Encrypt your message with the RSA public key (client-side)
550
+ 4. Send encrypted: `sendEncryptedSms(encryptedPayload, deviceId)`
551
+ 5. Device decrypts with its private key
552
+
553
+ ### Key Verification
554
+
555
+ Store the `publicKeyFingerprint` locally after first pairing. Before sending, verify it hasn't changed:
556
+
557
+ ```typescript
558
+ const result = await client.verifyDeviceKey('device-id', 'stored-fingerprint');
559
+ if (result.needsRePairing) {
560
+ // Device was reinstalled, key changed - need to re-pair
561
+ }
562
+ ```
563
+
564
+ ---
565
+
459
566
  ## SmsTunnelClient API (framework-agnostic)
460
567
 
461
568
  ```typescript
462
- import { SmsTunnelClient } from 'smstunnel-sdk';
569
+ import { SmsTunnelClient } from '@narcisbodea/smstunnel-sdk';
463
570
 
464
571
  const client = new SmsTunnelClient({
465
572
  apiBaseUrl: '/api',
@@ -468,14 +575,24 @@ const client = new SmsTunnelClient({
468
575
  pollInterval: 3000, // default
469
576
  });
470
577
 
578
+ // Pairing
471
579
  await client.getStatus(); // { paired, serverUrl, deviceName }
472
580
  await client.createToken(); // { success, token, qrData, ... }
473
581
  await client.getPairingStatus(token); // { status, displayName }
474
582
  client.startPolling(token, (event) => ...); // returns cleanup fn
475
583
  client.stopPolling();
476
584
  await client.unpair();
585
+
586
+ // SMS
477
587
  await client.sendSms('+40741234567', 'Hi');
478
588
  await client.updateServerUrl('https://...');
589
+
590
+ // E2E Encryption
591
+ await client.getDeviceE2EStatus('device-id');
592
+ await client.getDevicePublicKey('device-id');
593
+ await client.verifyDeviceKey('device-id', 'fingerprint');
594
+ await client.sendEncryptedSms('payload', 'device-id');
595
+
479
596
  client.destroy(); // cleanup
480
597
  ```
481
598
 
@@ -487,9 +604,9 @@ Built-in presets: `EN_LABELS` (default), `RO_LABELS`.
487
604
 
488
605
  Available from any import path:
489
606
  ```typescript
490
- import { EN_LABELS, RO_LABELS } from 'smstunnel-sdk';
491
- import { EN_LABELS, RO_LABELS } from 'smstunnel-sdk/react';
492
- import { EN_LABELS, RO_LABELS } from 'smstunnel-sdk/vue';
607
+ import { EN_LABELS, RO_LABELS } from '@narcisbodea/smstunnel-sdk';
608
+ import { EN_LABELS, RO_LABELS } from '@narcisbodea/smstunnel-sdk/react';
609
+ import { EN_LABELS, RO_LABELS } from '@narcisbodea/smstunnel-sdk/vue';
493
610
  ```
494
611
 
495
612
  Custom labels: implement the `SmsTunnelLabels` interface.
@@ -500,10 +617,12 @@ Custom labels: implement the `SmsTunnelLabels` interface.
500
617
 
501
618
  | Path | Exports |
502
619
  |------|---------|
503
- | `smstunnel-sdk` | `SmsTunnelClient`, `EN_LABELS`, `RO_LABELS`, types |
504
- | `smstunnel-sdk/server` | `SmsTunnelModule`, `SmsTunnelService`, `SMSTUNNEL_PUBLIC_PATHS` |
505
- | `smstunnel-sdk/react` | `SmsTunnelPairing`, `QrCodeCanvas`, `useSmsTunnel` |
506
- | `smstunnel-sdk/vue` | `SmsTunnelPairing`, `QrCodeCanvas`, `useSmsTunnel` |
620
+ | `@narcisbodea/smstunnel-sdk` | `SmsTunnelClient`, `EN_LABELS`, `RO_LABELS`, types |
621
+ | `@narcisbodea/smstunnel-sdk/server` | `SmsTunnelModule`, `SmsTunnelService`, `SMSTUNNEL_PUBLIC_PATHS` |
622
+ | `@narcisbodea/smstunnel-sdk/react` | `SmsTunnelPairing`, `QrCodeCanvas`, `useSmsTunnel` |
623
+ | `@narcisbodea/smstunnel-sdk/vue` | `SmsTunnelPairing`, `QrCodeCanvas`, `useSmsTunnel` |
624
+ | `@narcisbodea/smstunnel-sdk/angular` | `SmsTunnelService`, `provideSmsTunnel`, `SMSTUNNEL_CONFIG` |
625
+ | `@narcisbodea/smstunnel-sdk/node` | `SmsTunnelNodeClient` |
507
626
 
508
627
  ## License
509
628
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@narcisbodea/smstunnel-sdk",
3
- "version": "1.1.2",
4
- "description": "SMSTunnel SDK - Node.js + NestJS + React + Vue + framework-agnostic client for SMS pairing & E2E encryption",
3
+ "version": "1.1.4",
4
+ "description": "SMSTunnel SDK - Node.js + NestJS + React + Vue + Angular + framework-agnostic client for SMS pairing & E2E encryption",
5
5
  "author": "NARBOWEB SRL",
6
6
  "license": "MIT",
7
7
  "repository": {