@narcisbodea/smstunnel-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,490 @@
1
+ # smstunnel-sdk
2
+
3
+ SMSTunnel SDK - NestJS + React + Vue + framework-agnostic client for SMS pairing.
4
+
5
+ | Import path | Scope |
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) |
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install smstunnel-sdk
16
+ ```
17
+
18
+ ---
19
+
20
+ ## Server (NestJS)
21
+
22
+ ### 1. Implement a Storage Adapter
23
+
24
+ The SDK is database-agnostic. You provide a simple adapter with 3 methods:
25
+
26
+ #### MongoDB (Mongoose)
27
+
28
+ ```typescript
29
+ import { SmsTunnelStorageAdapter, SmsTunnelConfig } from 'smstunnel-sdk/server';
30
+ import { Model } from 'mongoose';
31
+
32
+ export class MongoSmsTunnelStorage implements SmsTunnelStorageAdapter {
33
+ constructor(private model: Model<any>) {}
34
+
35
+ async getConfig(): Promise<Partial<SmsTunnelConfig>> {
36
+ const doc = await this.model.findOne().lean().exec();
37
+ return {
38
+ serverUrl: doc?.smstunnelServerUrl || '',
39
+ apiKey: doc?.smstunnelApiKey || '',
40
+ siteToken: doc?.smstunnelSiteToken || '',
41
+ deviceName: doc?.smstunnelDeviceName || '',
42
+ };
43
+ }
44
+
45
+ async updateConfig(partial: Partial<SmsTunnelConfig>): Promise<void> {
46
+ const $set: Record<string, string> = {};
47
+ if (partial.serverUrl !== undefined) $set.smstunnelServerUrl = partial.serverUrl;
48
+ if (partial.apiKey !== undefined) $set.smstunnelApiKey = partial.apiKey;
49
+ if (partial.siteToken !== undefined) $set.smstunnelSiteToken = partial.siteToken;
50
+ if (partial.deviceName !== undefined) $set.smstunnelDeviceName = partial.deviceName;
51
+ await this.model.updateOne({}, { $set });
52
+ }
53
+
54
+ async clearConfig(): Promise<void> {
55
+ await this.model.updateOne({}, {
56
+ $set: { smstunnelApiKey: '', smstunnelDeviceName: '', smstunnelSiteToken: '' },
57
+ });
58
+ }
59
+ }
60
+ ```
61
+
62
+ #### Prisma (PostgreSQL)
63
+
64
+ ```typescript
65
+ import { SmsTunnelStorageAdapter, SmsTunnelConfig } from 'smstunnel-sdk/server';
66
+ import { PrismaClient } from '@prisma/client';
67
+
68
+ export class PrismaSmsTunnelStorage implements SmsTunnelStorageAdapter {
69
+ constructor(private prisma: PrismaClient) {}
70
+
71
+ async getConfig(): Promise<Partial<SmsTunnelConfig>> {
72
+ const row = await this.prisma.appSettings.findFirst();
73
+ return {
74
+ serverUrl: row?.smstunnelServerUrl || '',
75
+ apiKey: row?.smstunnelApiKey || '',
76
+ siteToken: row?.smstunnelSiteToken || '',
77
+ deviceName: row?.smstunnelDeviceName || '',
78
+ };
79
+ }
80
+
81
+ async updateConfig(partial: Partial<SmsTunnelConfig>): Promise<void> {
82
+ await this.prisma.appSettings.updateMany({ data: partial });
83
+ }
84
+
85
+ async clearConfig(): Promise<void> {
86
+ await this.prisma.appSettings.updateMany({
87
+ data: { apiKey: '', deviceName: '', siteToken: '' },
88
+ });
89
+ }
90
+ }
91
+ ```
92
+
93
+ #### JSON File
94
+
95
+ ```typescript
96
+ import { SmsTunnelStorageAdapter, SmsTunnelConfig } from 'smstunnel-sdk/server';
97
+ import { readFile, writeFile } from 'fs/promises';
98
+
99
+ export class JsonFileStorage implements SmsTunnelStorageAdapter {
100
+ constructor(private filePath: string) {}
101
+
102
+ async getConfig(): Promise<Partial<SmsTunnelConfig>> {
103
+ try { return JSON.parse(await readFile(this.filePath, 'utf-8')); }
104
+ catch { return {}; }
105
+ }
106
+
107
+ async updateConfig(partial: Partial<SmsTunnelConfig>): Promise<void> {
108
+ const config = { ...(await this.getConfig()), ...partial };
109
+ await writeFile(this.filePath, JSON.stringify(config, null, 2));
110
+ }
111
+
112
+ async clearConfig(): Promise<void> {
113
+ await this.updateConfig({ apiKey: '', deviceName: '', siteToken: '' });
114
+ }
115
+ }
116
+ ```
117
+
118
+ ### 2. Register the Module
119
+
120
+ ```typescript
121
+ import { SmsTunnelModule } from 'smstunnel-sdk/server';
122
+
123
+ @Module({
124
+ imports: [
125
+ SmsTunnelModule.forRoot({
126
+ storage: new MongoSmsTunnelStorage(settingsModel),
127
+ callbackBaseUrl: 'https://myapp.com/api',
128
+ displayName: 'My App',
129
+ }),
130
+ ],
131
+ })
132
+ export class AppModule {}
133
+ ```
134
+
135
+ Or async:
136
+
137
+ ```typescript
138
+ SmsTunnelModule.forRootAsync({
139
+ useFactory: (settingsModel) => ({
140
+ storage: new MongoSmsTunnelStorage(settingsModel),
141
+ callbackBaseUrl: process.env.CALLBACK_URL,
142
+ displayName: 'My App',
143
+ }),
144
+ inject: [getModelToken('Settings')],
145
+ })
146
+ ```
147
+
148
+ ### 3. Auth Guard Integration
149
+
150
+ ```typescript
151
+ import { SMSTUNNEL_PUBLIC_PATHS } from 'smstunnel-sdk/server';
152
+
153
+ // In your JwtAuthGuard:
154
+ if (SMSTUNNEL_PUBLIC_PATHS.some(p => request.url.includes(p))) return true;
155
+ ```
156
+
157
+ ### REST Endpoints
158
+
159
+ | Route | Method | Auth | Purpose |
160
+ |-------|--------|------|---------|
161
+ | `GET /smstunnel/status` | getStatus | Yes | Show pairing status |
162
+ | `POST /smstunnel/create-token` | createToken | Yes | Generate QR |
163
+ | `GET /smstunnel/pairing-status/:token` | pairingStatus | No | Polling proxy |
164
+ | `POST /smstunnel/callback` | callback | No | Receive API key |
165
+ | `POST /smstunnel/unpair` | unpair | Yes | Disconnect device |
166
+ | `POST /smstunnel/send` | sendSms | Yes | Send SMS |
167
+ | `POST /smstunnel/update-config` | updateConfig | Yes | Set server URL |
168
+
169
+ ---
170
+
171
+ ## React
172
+
173
+ Works with: **React, Next.js, Remix, Gatsby**
174
+
175
+ ### Plug and Play Component
176
+
177
+ ```tsx
178
+ import { SmsTunnelPairing, RO_LABELS } from 'smstunnel-sdk/react';
179
+
180
+ function SettingsPage() {
181
+ return (
182
+ <SmsTunnelPairing
183
+ apiBaseUrl="/api"
184
+ getAuthHeaders={() => ({
185
+ Authorization: `Bearer ${localStorage.getItem('token')}`,
186
+ })}
187
+ labels={RO_LABELS}
188
+ onPaired={(device) => console.log('Paired:', device)}
189
+ showTestSms={true}
190
+ showServerUrlInput={true}
191
+ qrSize={220}
192
+ />
193
+ );
194
+ }
195
+ ```
196
+
197
+ ### Next.js
198
+
199
+ Add `'use client'` in your page/component:
200
+
201
+ ```tsx
202
+ 'use client';
203
+ import { SmsTunnelPairing, RO_LABELS } from 'smstunnel-sdk/react';
204
+
205
+ export default function SmsSettings() {
206
+ return <SmsTunnelPairing apiBaseUrl="/api" labels={RO_LABELS} />;
207
+ }
208
+ ```
209
+
210
+ ### Custom UI with Hook
211
+
212
+ ```tsx
213
+ import { useSmsTunnel, QrCodeCanvas } from 'smstunnel-sdk/react';
214
+
215
+ function MySmsSettings() {
216
+ const tunnel = useSmsTunnel({
217
+ apiBaseUrl: '/api',
218
+ getAuthHeaders: () => ({ Authorization: `Bearer ${token}` }),
219
+ });
220
+
221
+ if (tunnel.status === 'paired') {
222
+ return <div>Connected: {tunnel.deviceName}</div>;
223
+ }
224
+
225
+ return (
226
+ <div>
227
+ {tunnel.showQr ? (
228
+ <>
229
+ <QrCodeCanvas value={tunnel.qrData} size={200} />
230
+ <button onClick={tunnel.cancelPairing}>Cancel</button>
231
+ </>
232
+ ) : (
233
+ <button onClick={tunnel.generateQr} disabled={tunnel.generating}>
234
+ Connect Phone
235
+ </button>
236
+ )}
237
+ </div>
238
+ );
239
+ }
240
+ ```
241
+
242
+ ---
243
+
244
+ ## Vue
245
+
246
+ Works with: **Vue 3, Nuxt 3**
247
+
248
+ ### Plug and Play Component
249
+
250
+ ```vue
251
+ <script setup>
252
+ import { SmsTunnelPairing, RO_LABELS } from 'smstunnel-sdk/vue';
253
+
254
+ function onPaired(device) {
255
+ console.log('Paired:', device);
256
+ }
257
+ </script>
258
+
259
+ <template>
260
+ <SmsTunnelPairing
261
+ api-base-url="/api"
262
+ :labels="RO_LABELS"
263
+ :show-test-sms="true"
264
+ :show-server-url-input="true"
265
+ @paired="onPaired"
266
+ />
267
+ </template>
268
+ ```
269
+
270
+ ### Nuxt 3
271
+
272
+ Wrap in `<ClientOnly>` since pairing uses browser APIs:
273
+
274
+ ```vue
275
+ <template>
276
+ <ClientOnly>
277
+ <SmsTunnelPairing api-base-url="/api" :labels="RO_LABELS" />
278
+ </ClientOnly>
279
+ </template>
280
+
281
+ <script setup>
282
+ import { SmsTunnelPairing, RO_LABELS } from 'smstunnel-sdk/vue';
283
+ </script>
284
+ ```
285
+
286
+ ### Custom UI with Composable
287
+
288
+ ```vue
289
+ <script setup>
290
+ import { useSmsTunnel, QrCodeCanvas } from 'smstunnel-sdk/vue';
291
+
292
+ const tunnel = useSmsTunnel({
293
+ apiBaseUrl: '/api',
294
+ getAuthHeaders: () => ({ Authorization: `Bearer ${token}` }),
295
+ });
296
+ </script>
297
+
298
+ <template>
299
+ <div v-if="tunnel.status.value === 'paired'">
300
+ Connected: {{ tunnel.deviceName.value }}
301
+ <button @click="tunnel.unpair()">Disconnect</button>
302
+ </div>
303
+ <div v-else>
304
+ <div v-if="tunnel.showQr.value">
305
+ <QrCodeCanvas :value="tunnel.qrData.value" :size="200" />
306
+ <button @click="tunnel.cancelPairing()">Cancel</button>
307
+ </div>
308
+ <button v-else @click="tunnel.generateQr()" :disabled="tunnel.generating.value">
309
+ Connect Phone
310
+ </button>
311
+ </div>
312
+ </template>
313
+ ```
314
+
315
+ ---
316
+
317
+ ## Angular
318
+
319
+ Use the framework-agnostic `SmsTunnelClient` class:
320
+
321
+ ```typescript
322
+ import { Injectable, OnDestroy } from '@angular/core';
323
+ import { SmsTunnelClient } from 'smstunnel-sdk';
324
+
325
+ @Injectable({ providedIn: 'root' })
326
+ export class SmsTunnelService implements OnDestroy {
327
+ private client = new SmsTunnelClient({
328
+ apiBaseUrl: '/api',
329
+ getAuthHeaders: () => ({
330
+ Authorization: `Bearer ${localStorage.getItem('token')}`,
331
+ }),
332
+ });
333
+
334
+ getStatus() { return this.client.getStatus(); }
335
+ createToken() { return this.client.createToken(); }
336
+ startPolling(token: string, cb: any) { return this.client.startPolling(token, cb); }
337
+ sendSms(to: string, msg: string) { return this.client.sendSms(to, msg); }
338
+ unpair() { return this.client.unpair(); }
339
+ updateServerUrl(url: string) { return this.client.updateServerUrl(url); }
340
+
341
+ ngOnDestroy() { this.client.destroy(); }
342
+ }
343
+ ```
344
+
345
+ ```typescript
346
+ // Component
347
+ @Component({ template: `...` })
348
+ export class SmsPairingComponent {
349
+ constructor(private sms: SmsTunnelService) {}
350
+
351
+ async connect() {
352
+ const result = await this.sms.createToken();
353
+ if (result.success) {
354
+ this.qrData = result.qrData;
355
+ this.sms.startPolling(result.token!, (event) => {
356
+ if (event === 'completed') this.loadStatus();
357
+ });
358
+ }
359
+ }
360
+ }
361
+ ```
362
+
363
+ ---
364
+
365
+ ## Svelte
366
+
367
+ ```svelte
368
+ <script>
369
+ import { SmsTunnelClient } from 'smstunnel-sdk';
370
+ import { onMount, onDestroy } from 'svelte';
371
+
372
+ let status = 'loading';
373
+ let qrData = '';
374
+ let deviceName = '';
375
+
376
+ const client = new SmsTunnelClient({
377
+ apiBaseUrl: '/api',
378
+ getAuthHeaders: () => ({
379
+ Authorization: `Bearer ${localStorage.getItem('token')}`,
380
+ }),
381
+ });
382
+
383
+ onMount(async () => {
384
+ const s = await client.getStatus();
385
+ status = s.paired ? 'paired' : 'unpaired';
386
+ deviceName = s.deviceName || '';
387
+ });
388
+
389
+ onDestroy(() => client.destroy());
390
+
391
+ async function connect() {
392
+ const result = await client.createToken();
393
+ if (result.success) {
394
+ qrData = result.qrData;
395
+ client.startPolling(result.token, async (event) => {
396
+ if (event === 'completed') {
397
+ const s = await client.getStatus();
398
+ status = 'paired';
399
+ deviceName = s.deviceName || '';
400
+ }
401
+ });
402
+ }
403
+ }
404
+ </script>
405
+
406
+ {#if status === 'paired'}
407
+ <p>Connected: {deviceName}</p>
408
+ <button on:click={() => client.unpair()}>Disconnect</button>
409
+ {:else}
410
+ <button on:click={connect}>Connect Phone</button>
411
+ {/if}
412
+ ```
413
+
414
+ ---
415
+
416
+ ## Astro
417
+
418
+ Astro supports React, Vue, and Svelte islands. Use any of the above:
419
+
420
+ ```astro
421
+ ---
422
+ // pages/settings.astro
423
+ ---
424
+ <script>
425
+ import { SmsTunnelPairing, RO_LABELS } from 'smstunnel-sdk/react';
426
+ </script>
427
+
428
+ <SmsTunnelPairing client:only="react" apiBaseUrl="/api" labels={RO_LABELS} />
429
+ ```
430
+
431
+ Or with Vue:
432
+
433
+ ```astro
434
+ <SmsTunnelPairing client:only="vue" api-base-url="/api" />
435
+ ```
436
+
437
+ ---
438
+
439
+ ## SmsTunnelClient API (framework-agnostic)
440
+
441
+ ```typescript
442
+ import { SmsTunnelClient } from 'smstunnel-sdk';
443
+
444
+ const client = new SmsTunnelClient({
445
+ apiBaseUrl: '/api',
446
+ getAuthHeaders: () => ({ Authorization: 'Bearer ...' }),
447
+ routePrefix: 'smstunnel', // default
448
+ pollInterval: 3000, // default
449
+ });
450
+
451
+ await client.getStatus(); // { paired, serverUrl, deviceName }
452
+ await client.createToken(); // { success, token, qrData, ... }
453
+ await client.getPairingStatus(token); // { status, displayName }
454
+ client.startPolling(token, (event) => ...); // returns cleanup fn
455
+ client.stopPolling();
456
+ await client.unpair();
457
+ await client.sendSms('+40741234567', 'Hi');
458
+ await client.updateServerUrl('https://...');
459
+ client.destroy(); // cleanup
460
+ ```
461
+
462
+ ---
463
+
464
+ ## Labels (i18n)
465
+
466
+ Built-in presets: `EN_LABELS` (default), `RO_LABELS`.
467
+
468
+ Available from any import path:
469
+ ```typescript
470
+ import { EN_LABELS, RO_LABELS } from 'smstunnel-sdk';
471
+ import { EN_LABELS, RO_LABELS } from 'smstunnel-sdk/react';
472
+ import { EN_LABELS, RO_LABELS } from 'smstunnel-sdk/vue';
473
+ ```
474
+
475
+ Custom labels: implement the `SmsTunnelLabels` interface.
476
+
477
+ ---
478
+
479
+ ## Exports Summary
480
+
481
+ | Path | Exports |
482
+ |------|---------|
483
+ | `smstunnel-sdk` | `SmsTunnelClient`, `EN_LABELS`, `RO_LABELS`, types |
484
+ | `smstunnel-sdk/server` | `SmsTunnelModule`, `SmsTunnelService`, `SMSTUNNEL_PUBLIC_PATHS` |
485
+ | `smstunnel-sdk/react` | `SmsTunnelPairing`, `QrCodeCanvas`, `useSmsTunnel` |
486
+ | `smstunnel-sdk/vue` | `SmsTunnelPairing`, `QrCodeCanvas`, `useSmsTunnel` |
487
+
488
+ ## License
489
+
490
+ MIT