webs-sdk 0.19.64 → 0.19.66

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 (101) hide show
  1. package/AGENTS.md +44 -0
  2. package/CLAUDE.md +80 -0
  3. package/README.md +513 -490
  4. package/dist/components/ARFilters/ARFilters.js +54 -54
  5. package/dist/components/ArtGenerator.js +141 -141
  6. package/dist/components/AvatarAI.js +14 -14
  7. package/dist/components/CookieConsent/constants.js +31 -31
  8. package/dist/components/CreativeFaceSwap.js +23 -23
  9. package/dist/components/GifsIa.js +76 -76
  10. package/dist/components/MemeGenerator.js +12 -12
  11. package/dist/components/PasswordReset/constants.js +33 -33
  12. package/dist/components/RecoveryPassword/constants.js +27 -27
  13. package/dist/components/SpinningWheel/SpinningWheel.js +7 -7
  14. package/dist/components/SpinningWheel/index.d.ts +1 -1
  15. package/dist/components/SpinningWheel/index.d.ts.map +1 -1
  16. package/dist/components/SpinningWheel/index.js +2 -1
  17. package/dist/components/SpinningWheel/index.js.map +1 -1
  18. package/dist/components/ui/ConfiguredMediaPlayer.d.ts +49 -0
  19. package/dist/components/ui/ConfiguredMediaPlayer.d.ts.map +1 -0
  20. package/dist/components/ui/ConfiguredMediaPlayer.js +122 -0
  21. package/dist/components/ui/ConfiguredMediaPlayer.js.map +1 -0
  22. package/dist/components/ui/badge.d.ts +1 -0
  23. package/dist/components/ui/badge.d.ts.map +1 -1
  24. package/dist/components/ui/badge.js +1 -1
  25. package/dist/components/ui/badge.js.map +1 -1
  26. package/dist/components/ui/compose-refs.d.ts +6 -0
  27. package/dist/components/ui/compose-refs.d.ts.map +1 -0
  28. package/dist/components/ui/compose-refs.js +57 -0
  29. package/dist/components/ui/compose-refs.js.map +1 -0
  30. package/dist/components/ui/dropdown-menu.d.ts +3 -1
  31. package/dist/components/ui/dropdown-menu.d.ts.map +1 -1
  32. package/dist/components/ui/dropdown-menu.js +2 -2
  33. package/dist/components/ui/dropdown-menu.js.map +1 -1
  34. package/dist/components/ui/index.d.ts +2 -0
  35. package/dist/components/ui/index.d.ts.map +1 -1
  36. package/dist/components/ui/index.js +2 -0
  37. package/dist/components/ui/index.js.map +1 -1
  38. package/dist/components/ui/media-player.d.ts +127 -0
  39. package/dist/components/ui/media-player.d.ts.map +1 -0
  40. package/dist/components/ui/media-player.js +1633 -0
  41. package/dist/components/ui/media-player.js.map +1 -0
  42. package/dist/components/ui/tooltip.d.ts +3 -1
  43. package/dist/components/ui/tooltip.d.ts.map +1 -1
  44. package/dist/components/ui/tooltip.js +2 -2
  45. package/dist/components/ui/tooltip.js.map +1 -1
  46. package/dist/components/ui/use-lazy-ref.d.ts +4 -0
  47. package/dist/components/ui/use-lazy-ref.d.ts.map +1 -0
  48. package/dist/components/ui/use-lazy-ref.js +45 -0
  49. package/dist/components/ui/use-lazy-ref.js.map +1 -0
  50. package/dist/index.d.ts +1 -1
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +4 -3
  53. package/dist/index.js.map +1 -1
  54. package/dist/network.d.ts +12 -0
  55. package/dist/network.d.ts.map +1 -0
  56. package/dist/network.js +15 -0
  57. package/dist/network.js.map +1 -0
  58. package/dist/server.d.ts +54 -0
  59. package/dist/server.d.ts.map +1 -0
  60. package/dist/server.js +74 -0
  61. package/dist/server.js.map +1 -0
  62. package/dist/types/media-chrome-store-types.d.ts +107 -0
  63. package/dist/types/media-chrome-store-types.d.ts.map +1 -0
  64. package/dist/types/media-chrome-store-types.js +3 -0
  65. package/dist/types/media-chrome-store-types.js.map +1 -0
  66. package/dist/utils/QRCode.d.ts +1 -0
  67. package/dist/utils/QRCode.d.ts.map +1 -0
  68. package/dist/utils/QRCode.js +2 -0
  69. package/dist/utils/QRCode.js.map +1 -0
  70. package/dist/utils/imageValidation.d.ts +6 -0
  71. package/dist/utils/imageValidation.d.ts.map +1 -0
  72. package/dist/utils/imageValidation.js +64 -0
  73. package/dist/utils/imageValidation.js.map +1 -0
  74. package/docs/CONFIGURED_MEDIA_PLAYER.md +40 -0
  75. package/docs/CORP_AUTH_USAGE.md +687 -0
  76. package/docs/EXAMPLES_WALLPAPERSNAME_IMPLEMENTATION.md +1445 -0
  77. package/docs/EXAMPLE_ENDPOINTS_COMUNICATION.md +55 -0
  78. package/docs/EXTERNAL_SDK_GUIDE.md +1340 -0
  79. package/docs/FOR_YOU_360_FACESWAP_FIX.md +400 -0
  80. package/docs/FOR_YOU_IMPLEMENTATION_GUIDE.md +1413 -0
  81. package/docs/FOR_YOU_QUIZ_ARCHITECTURE.md +753 -0
  82. package/docs/FOR_YOU_SDK_WALLPAPERSNAME_BUG.md +106 -0
  83. package/docs/FOR_YOU_WALLPAPERSNAME_ENHANCEMENT_PLAN.md +210 -0
  84. package/docs/LEGAL_USAGE.md +190 -0
  85. package/docs/STREAK_IMPLEMENTATION_GUIDE.md +430 -0
  86. package/package.json +106 -84
  87. package/public/spin2win/assets/border-afristream.svg +26 -26
  88. package/public/spin2win/assets/border-efc.svg +23 -23
  89. package/public/spin2win/assets/border-streetreel.svg +23 -23
  90. package/public/spin2win/assets/border-totalgym.svg +27 -27
  91. package/public/spin2win/assets/border-womantoday.svg +26 -26
  92. package/public/spin2win/assets/indicator-afristream.svg +28 -28
  93. package/public/spin2win/assets/indicator-efc.svg +29 -29
  94. package/public/spin2win/assets/indicator-streetreel.svg +29 -29
  95. package/public/spin2win/assets/indicator-totalgym.svg +29 -29
  96. package/public/spin2win/fonts/switzer/README.md +22 -22
  97. package/scripts/install-agent-skill.cjs +34 -0
  98. package/skills/webs-sdk-package/SKILL.md +21 -0
  99. package/skills/webs-sdk-package/references/components.md +32 -0
  100. package/skills/webs-sdk-package/references/integration.md +32 -0
  101. package/skills/webs-sdk-package/references/ui.md +197 -0
package/README.md CHANGED
@@ -1,490 +1,513 @@
1
- # webs-sdk
2
-
3
- SDK interno npm para portales Nexum. Encapsula toda la lógica común: autenticación, sesiones, comunicación con la API, tracking, internacionalización y componentes React reutilizables.
4
-
5
- **Versión actual:** 0.18.9
6
-
7
- ## Instalación
8
-
9
- ```bash
10
- npm install webs-sdk
11
- ```
12
-
13
- ## Uso básico
14
-
15
- ```typescript
16
- import WebsSDK from 'webs-sdk';
17
-
18
- // Inicializar sesión
19
- await WebsSDK.Session.init();
20
-
21
- // Petición HTTP
22
- const data = await WebsSDK.Networking.request('/api/endpoint', { param: 'value' });
23
-
24
- // Almacenamiento local
25
- WebsSDK.Storage.set('key', 'value');
26
- const value = WebsSDK.Storage.get('key');
27
- ```
28
-
29
- ## Librerías disponibles
30
-
31
- ### Networking
32
-
33
- ```typescript
34
- // Petición HTTP (inyecta sessionData automáticamente)
35
- await WebsSDK.Networking.request(url, data);
36
-
37
- // Eventos de tracking
38
- await WebsSDK.Networking.sendEvent('action', 'button_click', { button_id: 'submit' });
39
-
40
- // Suscripciones
41
- await WebsSDK.Networking.createSubscription(subscriptionData);
42
- const { success, subscription_active } = await WebsSDK.Networking.checkSubscription();
43
- ```
44
-
45
- ### Storage
46
-
47
- ```typescript
48
- WebsSDK.Storage.set('key', 'value');
49
- const value = WebsSDK.Storage.get('key');
50
-
51
- // Compresión de imágenes (0.8 ratio, 1024px max)
52
- const compressed = await WebsSDK.Storage.compressImage(imageFile, 2); // 2MB max
53
-
54
- // Creaciones (imágenes generadas por el usuario)
55
- await WebsSDK.Storage.handleDownloadImageToCreations(base64, 'id', metadata);
56
- const creations = await WebsSDK.Storage.getCreations();
57
- await WebsSDK.Storage.deleteCreation('id');
58
- ```
59
-
60
- ### Session
61
-
62
- ```typescript
63
- await WebsSDK.Session.init();
64
-
65
- const sessionData = WebsSDK.Session.getSessionData();
66
- const userId = WebsSDK.Session.getUserID();
67
- const lang = WebsSDK.Session.getDeviceLanguage();
68
- const isSubscribed = WebsSDK.Session.getIsSubscribed();
69
- ```
70
-
71
- ### Utils
72
-
73
- ```typescript
74
- const id = WebsSDK.Utils.generateId();
75
- const isValidEmail = WebsSDK.Utils.isValidEmail('user@example.com');
76
- const isMobile = WebsSDK.Utils.isMobile();
77
- const deviceInfo = WebsSDK.Utils.getDeviceInfo();
78
- const truncated = WebsSDK.Utils.truncateText('Long text...', 10);
79
- ```
80
-
81
- ### MixPanel
82
-
83
- ```typescript
84
- await WebsSDK.MixPanel.initialize(token, false, false, false);
85
- await WebsSDK.MixPanel.trackEvent('page_view', { page: 'home' });
86
- await WebsSDK.MixPanel.identifyUser('user_123');
87
- await WebsSDK.MixPanel.setUserProperties({ plan: 'premium' });
88
- ```
89
-
90
- ### Clarity — Microsoft Clarity
91
-
92
- Heatmaps y session recordings. El `projectId` de cada portal viaja en su `/config`
93
- (`product_config.tracking_id`). Cómo se entrega a Clarity depende del entorno:
94
-
95
- - **App móvil**: arranca solo. `Session.init()` → `Networking.executeInit()` lee
96
- `product_config.tracking_id` e inicializa Clarity, identificando con `getUserID()`.
97
- - **Portal web**: `executeInit()` no corre (el config llega por el proxy /
98
- `WebsiteProvider` del portal). El portal debe inicializar Clarity con un
99
- `ClarityInitializer` (espejo del `MixpanelInitializer`) que tome el `tracking_id`
100
- del `WebsiteProvider` y llame a `initialize()`.
101
-
102
- ```typescript
103
- // Init con opciones
104
- await WebsSDK.Clarity.initialize(projectId, {
105
- devMode: false, // true → omite la carga (desarrollo)
106
- requireConsent: true, // true → espera TRACKING_PERMISSION==='granted' (con banner)
107
- // false → arranca sin esperar consentimiento (sin banner)
108
- });
109
-
110
- // Consentimiento de cookies (desde el banner)
111
- await WebsSDK.Clarity.updateConsent(true); // false para rechazar
112
-
113
- // Identify: automático en Session.init() con getUserID(); manual opcional
114
- WebsSDK.Clarity.identify(userId);
115
- WebsSDK.Clarity.setTag('plan', 'premium');
116
- WebsSDK.Clarity.trackEvent('checkout_started');
117
- ```
118
-
119
- - **Consentimiento:** con `requireConsent: true` respeta `TRACKING_PERMISSION`
120
- (igual que GA4): si no está `'granted'`, no carga; queda pendiente y arranca al
121
- llamar `updateConsent(true)`. Con `requireConsent: false` (tenants sin banner)
122
- arranca directo.
123
- - **Identify diferido:** no importa el orden de init vs `Session.init()` ni cuándo
124
- llegue el consentimiento — el `identify(getUserID())` se aplica en cuanto Clarity
125
- está listo.
126
- - **Dev mode:** con `devMode: true` no carga (no ensucia Clarity con sesiones de
127
- desarrollo). El auto-init móvil usa `DEBUG_MODE` para esto.
128
-
129
- ### AuthManager
130
-
131
- ```typescript
132
- // Login estándar
133
- await WebsSDK.AuthManager.authUser({ login, password, website_id });
134
-
135
- // Login sin password (MSISDN)
136
- await WebsSDK.AuthManager.authOnlyUser({ login, website_id });
137
-
138
- // Login con OTP
139
- await WebsSDK.AuthManager.authOnlyUserOTP({ login, website_id, pincode });
140
-
141
- // Registro
142
- await WebsSDK.AuthManager.createUser({ login, password, website_id });
143
-
144
- // Auto-login post-pago (usado en el proxy de next_template)
145
- await WebsSDK.AuthManager.auto_login(cfg_sessionid, websiteData, requestUrl);
146
-
147
- // Baja/cancelación
148
- await WebsSDK.AuthManager.unsubscribeUser({ user_id, callback_url?, org? });
149
-
150
- // Gestión de perfil
151
- await WebsSDK.AuthManager.setUserMetadata(params);
152
- await WebsSDK.AuthManager.uploadProfilePhoto(params); // JPG/PNG/WEBP, max 5MB
153
- await WebsSDK.AuthManager.changePassword(params);
154
- ```
155
-
156
- ### Andromeda — Portales
157
-
158
- ```typescript
159
- // Metadata del portal (optimizado con cookie websiteDataFlag)
160
- await WebsSDK.Andromeda.getWebsiteMetadatabyHostname('portal.com');
161
-
162
- // Configuración del producto (langs, login_type, logo, favicon, theme)
163
- await WebsSDK.Andromeda.getWebsiteConfig('portal-corp-web');
164
- ```
165
-
166
- ### ContentManager
167
-
168
- ```typescript
169
- // Contenido por preset
170
- await WebsSDK.ContentManager.getContentByPreset('homepage', { lang: 'es' });
171
-
172
- // Colección filtrada por tags
173
- await WebsSDK.ContentManager.getCollection(['tag1', 'tag2'], 'es', false);
174
- ```
175
-
176
- ### User
177
-
178
- ```typescript
179
- await WebsSDK.User.checkSession(token); // Valida token (usado en middleware)
180
- await WebsSDK.User.getUserData(user_id, website_id);
181
- await WebsSDK.User.getCredits(user_id, website_id);
182
- ```
183
-
184
- ### CorporateAuthManager
185
-
186
- ```typescript
187
- // Server-side (middleware Next.js)
188
- const result = await WebsSDK.CorporateAuthManager.validateCorporateAuthServerSide(request);
189
- if (!result.authenticated) return result.redirectResponse;
190
-
191
- // Client-side
192
- await WebsSDK.CorporateAuthManager.checkCorporateLogin();
193
- await WebsSDK.CorporateAuthManager.validateUser(autoRedirect?);
194
- await WebsSDK.CorporateAuthManager.login(returnTo?);
195
- await WebsSDK.CorporateAuthManager.logout(redirectTo?);
196
- await WebsSDK.CorporateAuthManager.isAdmin(userData?);
197
- ```
198
-
199
- ### Legal
200
-
201
- ```typescript
202
- await WebsSDK.Legal.getLegalText('tos' | 'privacy' | 'faq', web_id, lang);
203
- ```
204
-
205
- ### Namespace `pre` — Pre-producción
206
-
207
- ```typescript
208
- // Lazy-loaded, apunta a bc1742-pre.gways.org
209
- await WebsSDK.pre.AuthManager.authUser(params);
210
- await WebsSDK.pre.Andromeda.getWebsiteMetadatabyHostname(hostname);
211
- ```
212
-
213
- ---
214
-
215
- ## Componentes React
216
-
217
- ### CookieConsent — Banner GDPR
218
-
219
- ```typescript
220
- import { CookieConsent, getCookieConsentTexts } from 'webs-sdk';
221
- import type { CookieConsentProps, CookieConsentTheme, CookieConsentPreferences } from 'webs-sdk';
222
-
223
- <CookieConsent
224
- locale="es" // en | es | fr | nl | ar | de | fi | ro | tr
225
- privacyPolicyUrl="/privacy"
226
- theme={{
227
- backgroundColor: '#1a1a2e',
228
- accentColor: '#6c63ff', // aplica a botón Accept, borde Reject, links y glow
229
- acceptButtonBg: 'linear-gradient(90deg, #6c63ff, #a855f7)', // soporta gradients CSS
230
- textColor: '#ffffff',
231
- }}
232
- onAccept={() => updateGAConsent('granted')}
233
- onReject={() => updateGAConsent('denied')}
234
- onSaveSettings={(prefs: CookieConsentPreferences) => handlePrefs(prefs)}
235
- />
236
- ```
237
-
238
- **Storage:**
239
- - Cookie `cookieConsent`: `accepted | rejected | custom` (1 año)
240
- - `localStorage.TRACKING_PERMISSION`: `granted | denied`
241
- - `localStorage.FUNCTIONAL_PERMISSION`: `granted | denied`
242
-
243
- ### Google Analytics — GA4
244
-
245
- ```typescript
246
- import { initGoogleAnalytics, updateGAConsent, trackGAEvent, trackGAPageView, isGALoaded } from 'webs-sdk';
247
-
248
- // Registra el GA ID. Carga el script SOLO si hay consentimiento previo.
249
- await initGoogleAnalytics('G-XXXXXXXXXX', { sendPageView: true, debug: false });
250
-
251
- // Llamar tras decisión del usuario en CookieConsent
252
- updateGAConsent('granted'); // carga GA si había pendingTrackingId
253
- updateGAConsent('denied'); // GA nunca se carga
254
-
255
- // Tracking
256
- trackGAEvent('purchase', { value: 9.99, currency: 'EUR' });
257
- trackGAPageView('/home', 'Inicio');
258
- isGALoaded(); // boolean
259
- ```
260
-
261
- > **GDPR estricto:** GA no usa Consent Mode v2. Si no hay consentimiento, el script `gtag.js` nunca se inyecta en el DOM.
262
-
263
- ### CDN Proxy
264
-
265
- ```typescript
266
- import { handleCDNRequest } from 'webs-sdk';
267
- import type { CDNOptions } from 'webs-sdk';
268
-
269
- // Usar en un route handler de Next.js (compatible con Edge Runtime)
270
- async function handleCDNRequest(
271
- pathname: string, // '/cdn/apariencias/33702/image.png'
272
- options?: {
273
- cdnBaseUrl?: string; // Default: 'https://dy822md8ge77v.cloudfront.net'
274
- cacheSeconds?: number; // Default: 300 (5 min)
275
- }
276
- ): Promise<Response | null> // null si pathname no empieza por /cdn/
277
- ```
278
-
279
- ### Componentes "For You" — Creación de contenido AI
280
-
281
- ```typescript
282
- import {
283
- AvatarAI, AvatarAIPage, AvatarAIForYouCard,
284
- CreativeFaceSwap, CreativeFaceSwapPage, CreativeFaceSwapForYouCard,
285
- MemeGenerator, MemeGeneratorPage, MemeGeneratorForYouCard,
286
- WallpapersName, WallpapersNamePage, WallpapersNameForYouCard,
287
- Wallpapers,
288
- Ringtone,
289
- } from 'webs-sdk';
290
- ```
291
-
292
- Todos aceptan `websiteId?: string` para tracking multi-portal.
293
-
294
- ### ARFilters — Filtros AR con DeepAR
295
-
296
- ```typescript
297
- import { ARFilters, ARFiltersForYouCard } from 'webs-sdk';
298
- import type { DeepARProps, DeepARFilter, DeepARTexts } from 'webs-sdk';
299
-
300
- <ARFilters
301
- licenseKey="deepar-license-key"
302
- websiteId="portal-web-id"
303
- apiBaseUrl="https://api.fluver-ai.com"
304
- downloadProxyUrl="/cdn" // Proxy CORS para descarga de efectos
305
- filterTag="ar-filters-tag" // Obtiene filtros del CMS por tag
306
- // O bien: filters={[{ file: 'beauty', thumbUrl: '/thumb.jpg' }]}
307
- locale="es"
308
- texts={getARFiltersTexts('es')} // requerido
309
- onPhotoTaken={(url) => share(url)}
310
- onVideoRecorded={(url) => share(url)}
311
- />
312
- ```
313
-
314
- Soporta fullscreen en mobile (vendor-prefixed API para iOS Safari).
315
-
316
- ### SpinningWheel — Ruleta
317
-
318
- ```typescript
319
- import { SpinningWheel, wheelThemes } from 'webs-sdk';
320
-
321
- // Temas: vodacom | totalgym | womantoday | afristream | efc
322
- <SpinningWheel
323
- userId={userId}
324
- websiteId={websiteId}
325
- theme="vodacom"
326
- onResult={(segment) => handleWin(segment)}
327
- canSpin={true}
328
- alreadySpunToday={false}
329
- />
330
- ```
331
-
332
- ### Quiz y Stickers
333
-
334
- ```typescript
335
- import { Quiz, Stickers } from 'webs-sdk';
336
- ```
337
-
338
- ### Esports — Noticias, Vídeos y Partidas en Directo
339
-
340
- ```typescript
341
- import { EsportsNews, EsportsVideos, EsportsLive } from 'webs-sdk';
342
- ```
343
-
344
- #### EsportsNews
345
-
346
- ```typescript
347
- <EsportsNews
348
- config={{
349
- key: 'api-key',
350
- contents: [{ preset: 'esports_news', apiProviderId: 'id', category: [], tags: [], limit: 10 }]
351
- }}
352
- language="es"
353
- onNewsClick={(item) => router.push(`/news/${item.id}`)}
354
- theme={theme} // CSS classes para container, cards, badges, etc.
355
- categoryStyles={{ gaming: { badge: 'bg-blue-500', badgeText: 'text-white' } }}
356
- // Modo detalle (renderiza EsportsNewsDetail):
357
- newsId="article-id"
358
- newsData={item} // opcional si ya tienes los datos
359
- onBack={() => router.back()}
360
- />
361
- ```
362
-
363
- #### EsportsVideos
364
-
365
- ```typescript
366
- <EsportsVideos
367
- config={{ key: 'api-key', contents: [...] }}
368
- language="es"
369
- onVideoClick={(item) => openPlayer(item.video_url)}
370
- theme={theme} // incluye clases para duration overlay
371
- />
372
- ```
373
-
374
- #### EsportsLive
375
-
376
- ```typescript
377
- <EsportsLive
378
- language="es"
379
- timezone="Europe/Madrid"
380
- onLiveClick={(item) => openStream(item.streamUrl)}
381
- onMatchClick={(match) => openMatchDetail(match)}
382
- onGameClick={(gameId) => filterByGame(gameId)}
383
- website_id="portal-web-id"
384
- theme={theme}
385
- // Modo detalle de partida:
386
- matchId="serie-id"
387
- matchData={calendarMatch}
388
- onBack={() => setMatchId(null)}
389
- />
390
- ```
391
-
392
- ---
393
-
394
- ## Utilidades de validación de vídeo
395
-
396
- ```typescript
397
- import { validateVideoUrl, validateVideoBatch, clearValidationCache } from 'webs-sdk';
398
- import { useValidateVideoOnMount, useLazyVideoValidation } from 'webs-sdk';
399
-
400
- // Función pura (sin React)
401
- const state = await validateVideoUrl('https://stream.example.com/live.m3u8');
402
- // 'valid' | 'invalid' | 'unknown'
403
-
404
- // Batch con concurrencia 3
405
- const results = await validateVideoBatch([url1, url2, url3]);
406
-
407
- // React: validar al montar (evita renderizar streams rotos)
408
- const { isValid, isInvalid, isValidating } = useValidateVideoOnMount(videoUrl);
409
-
410
- // React: validación manual
411
- const { validate, isValid, isInvalid } = useLazyVideoValidation();
412
- await validate(videoUrl);
413
- ```
414
-
415
- Soporta HLS, DASH, MP4. Cache en memoria con TTL de 5 minutos.
416
-
417
- ---
418
-
419
- ## Estructura del proyecto
420
-
421
- ```
422
- src/
423
- ├── config.ts # Configuración central (URLs, endpoints, constantes)
424
- ├── index.ts # Barrel export principal
425
- ├── libraries/
426
- │ ├── networking.ts # HTTP + events
427
- │ ├── auth.ts # Autenticación, login, baja
428
- │ ├── session.ts # Sesiones y dispositivo
429
- │ ├── storage.ts # LocalStorage + compresión de imágenes
430
- │ ├── utils.ts # Utilidades generales
431
- │ ├── andromeda.ts # Metadata y config de portales
432
- │ ├── corp_auth.ts # Auth corporativa (JWT)
433
- │ ├── user.ts # Datos de usuario
434
- │ ├── mixpanel.ts # Analytics
435
- │ ├── audio.ts # Text-to-speech
436
- │ ├── calypso.ts # Event tracking interno
437
- │ ├── legal.ts # Textos legales
438
- │ ├── content.ts # Contenido dinámico + colecciones por tags
439
- │ ├── cdn.ts # CDN Proxy (Edge Runtime compatible)
440
- │ └── googleAnalytics.ts # GA4 con carga diferida por consentimiento
441
- ├── utils/
442
- │ ├── videoValidation.ts # Validación de URLs de streaming
443
- │ ├── useVideoValidation.ts # React hooks para validación
444
- │ └── timeUtils.ts # getTimeAgo — tiempo relativo i18n
445
- └── components/
446
- ├── AvatarAI.tsx
447
- ├── CreativeFaceSwap.tsx
448
- ├── Wallpapers.tsx
449
- ├── WallpapersName.tsx
450
- ├── Ringtone.tsx
451
- ├── MemeGenerator.tsx
452
- ├── Quiz.tsx
453
- ├── Stickers.tsx
454
- ├── VideoPlayer.tsx
455
- ├── ARFilters/
456
- ├── SpinningWheel/
457
- ├── CookieConsent/
458
- └── esports/
459
- ├── news/
460
- ├── videos/
461
- └── live/
462
- ```
463
-
464
- ## Desarrollo
465
-
466
- ```bash
467
- npm install
468
- npm run build # tsc → dist/
469
- npm run build:watch # watch mode
470
- npm run lint
471
- ```
472
-
473
- ## Publicación
474
-
475
- ```bash
476
- # Incluir [ROAD-361723] en el commit para auto-publish en CI
477
- git commit -m "feat: descripción [ROAD-361723]"
478
- git push
479
- ```
480
-
481
- Tras publicar, actualizar en los portales:
482
- ```bash
483
- npm install webs-sdk@<nueva-version>
484
- ```
485
-
486
- **Nota importante — `index.js` raíz:** es el barrel CommonJS mantenido manualmente. Cada export nuevo en `src/index.ts` debe añadirse también en `index.js`:
487
- ```js
488
- const { nuevaFuncion } = require('./dist/index.js');
489
- module.exports.nuevaFuncion = nuevaFuncion;
490
- ```
1
+ # webs-sdk
2
+
3
+ SDK interno npm para portales Nexum. Encapsula toda la lógica común: autenticación, sesiones, comunicación con la API, tracking, internacionalización y componentes React reutilizables.
4
+
5
+ **Versión actual:** consulta `package.json` (CI gestiona la versión publicada).
6
+
7
+ **Agentes:** el paquete incluye `AGENTS.md`, guías en `docs/` y una skill instalable. En un portal:
8
+
9
+ ```bash
10
+ npm install webs-sdk
11
+ npm exec -- webs-sdk-skill # copia .agents/skills/webs-sdk-package al proyecto
12
+ ```
13
+
14
+ Reinicia el agente para descubrirla; vuelve a copiar tras actualizar (`npm exec -- webs-sdk-skill --force`). Pi también permite `pi install npm:webs-sdk --local` para cargar la skill sin copiarla (además de `npm install webs-sdk` para el portal). No actives ambas variantes de la skill a la vez. Instalar solo la dependencia no activa skills de forma automática en todos los agentes. Si no instalas la skill, pide al agente leer `node_modules/webs-sdk/AGENTS.md` y los `.d.ts` instalados para los exports reales de esa versión. Véase [`skills/webs-sdk-package/references/components.md`](skills/webs-sdk-package/references/components.md) para el catálogo de componentes y [`docs/CONFIGURED_MEDIA_PLAYER.md`](docs/CONFIGURED_MEDIA_PLAYER.md) para el reproductor.
15
+
16
+ ## Instalación
17
+
18
+ ```bash
19
+ npm install webs-sdk
20
+ ```
21
+
22
+ ## Uso básico
23
+
24
+ ```typescript
25
+ import WebsSDK from 'webs-sdk';
26
+
27
+ // Inicializar sesión
28
+ await WebsSDK.Session.init();
29
+
30
+ // Petición HTTP
31
+ const data = await WebsSDK.Networking.request('/api/endpoint', { param: 'value' });
32
+
33
+ // Almacenamiento local
34
+ WebsSDK.Storage.set('key', 'value');
35
+ const value = WebsSDK.Storage.get('key');
36
+ ```
37
+
38
+ ## Reproductor configurable (Radix)
39
+
40
+ ```tsx
41
+ import { ConfiguredMediaPlayer } from 'webs-sdk/ui';
42
+
43
+ <ConfiguredMediaPlayer
44
+ src="https://example.com/video.mp4"
45
+ poster="/poster.jpg"
46
+ controls={{ seekButtons: true, captions: true, pictureInPicture: true }}
47
+ />
48
+ ```
49
+
50
+ El `VideoPlayer` anterior sigue disponible; `MediaPlayer` y sus subcomponentes permiten layouts personalizados. Consulta [`docs/CONFIGURED_MEDIA_PLAYER.md`](docs/CONFIGURED_MEDIA_PLAYER.md) para props, HLS, pistas, idiomas y estilos Tailwind.
51
+
52
+ ## Librerías disponibles
53
+
54
+ ### Networking
55
+
56
+ ```typescript
57
+ // Petición HTTP (inyecta sessionData automáticamente)
58
+ await WebsSDK.Networking.request(url, data);
59
+
60
+ // Eventos de tracking
61
+ await WebsSDK.Networking.sendEvent('action', 'button_click', { button_id: 'submit' });
62
+
63
+ // Suscripciones
64
+ await WebsSDK.Networking.createSubscription(subscriptionData);
65
+ const { success, subscription_active } = await WebsSDK.Networking.checkSubscription();
66
+ ```
67
+
68
+ ### Storage
69
+
70
+ ```typescript
71
+ WebsSDK.Storage.set('key', 'value');
72
+ const value = WebsSDK.Storage.get('key');
73
+
74
+ // Compresión de imágenes (0.8 ratio, 1024px max)
75
+ const compressed = await WebsSDK.Storage.compressImage(imageFile, 2); // 2MB max
76
+
77
+ // Creaciones (imágenes generadas por el usuario)
78
+ await WebsSDK.Storage.handleDownloadImageToCreations(base64, 'id', metadata);
79
+ const creations = await WebsSDK.Storage.getCreations();
80
+ await WebsSDK.Storage.deleteCreation('id');
81
+ ```
82
+
83
+ ### Session
84
+
85
+ ```typescript
86
+ await WebsSDK.Session.init();
87
+
88
+ const sessionData = WebsSDK.Session.getSessionData();
89
+ const userId = WebsSDK.Session.getUserID();
90
+ const lang = WebsSDK.Session.getDeviceLanguage();
91
+ const isSubscribed = WebsSDK.Session.getIsSubscribed();
92
+ ```
93
+
94
+ ### Utils
95
+
96
+ ```typescript
97
+ const id = WebsSDK.Utils.generateId();
98
+ const isValidEmail = WebsSDK.Utils.isValidEmail('user@example.com');
99
+ const isMobile = WebsSDK.Utils.isMobile();
100
+ const deviceInfo = WebsSDK.Utils.getDeviceInfo();
101
+ const truncated = WebsSDK.Utils.truncateText('Long text...', 10);
102
+ ```
103
+
104
+ ### MixPanel
105
+
106
+ ```typescript
107
+ await WebsSDK.MixPanel.initialize(token, false, false, false);
108
+ await WebsSDK.MixPanel.trackEvent('page_view', { page: 'home' });
109
+ await WebsSDK.MixPanel.identifyUser('user_123');
110
+ await WebsSDK.MixPanel.setUserProperties({ plan: 'premium' });
111
+ ```
112
+
113
+ ### Clarity — Microsoft Clarity
114
+
115
+ Heatmaps y session recordings. El `projectId` de cada portal viaja en su `/config`
116
+ (`product_config.tracking_id`). Cómo se entrega a Clarity depende del entorno:
117
+
118
+ - **App móvil**: arranca solo. `Session.init()` → `Networking.executeInit()` lee
119
+ `product_config.tracking_id` e inicializa Clarity, identificando con `getUserID()`.
120
+ - **Portal web**: `executeInit()` no corre (el config llega por el proxy /
121
+ `WebsiteProvider` del portal). El portal debe inicializar Clarity con un
122
+ `ClarityInitializer` (espejo del `MixpanelInitializer`) que tome el `tracking_id`
123
+ del `WebsiteProvider` y llame a `initialize()`.
124
+
125
+ ```typescript
126
+ // Init con opciones
127
+ await WebsSDK.Clarity.initialize(projectId, {
128
+ devMode: false, // true → omite la carga (desarrollo)
129
+ requireConsent: true, // true → espera TRACKING_PERMISSION==='granted' (con banner)
130
+ // false → arranca sin esperar consentimiento (sin banner)
131
+ });
132
+
133
+ // Consentimiento de cookies (desde el banner)
134
+ await WebsSDK.Clarity.updateConsent(true); // false para rechazar
135
+
136
+ // Identify: automático en Session.init() con getUserID(); manual opcional
137
+ WebsSDK.Clarity.identify(userId);
138
+ WebsSDK.Clarity.setTag('plan', 'premium');
139
+ WebsSDK.Clarity.trackEvent('checkout_started');
140
+ ```
141
+
142
+ - **Consentimiento:** con `requireConsent: true` respeta `TRACKING_PERMISSION`
143
+ (igual que GA4): si no está `'granted'`, no carga; queda pendiente y arranca al
144
+ llamar `updateConsent(true)`. Con `requireConsent: false` (tenants sin banner)
145
+ arranca directo.
146
+ - **Identify diferido:** no importa el orden de init vs `Session.init()` ni cuándo
147
+ llegue el consentimiento — el `identify(getUserID())` se aplica en cuanto Clarity
148
+ está listo.
149
+ - **Dev mode:** con `devMode: true` no carga (no ensucia Clarity con sesiones de
150
+ desarrollo). El auto-init móvil usa `DEBUG_MODE` para esto.
151
+
152
+ ### AuthManager
153
+
154
+ ```typescript
155
+ // Login estándar
156
+ await WebsSDK.AuthManager.authUser({ login, password, website_id });
157
+
158
+ // Login sin password (MSISDN)
159
+ await WebsSDK.AuthManager.authOnlyUser({ login, website_id });
160
+
161
+ // Login con OTP
162
+ await WebsSDK.AuthManager.authOnlyUserOTP({ login, website_id, pincode });
163
+
164
+ // Registro
165
+ await WebsSDK.AuthManager.createUser({ login, password, website_id });
166
+
167
+ // Auto-login post-pago (usado en el proxy de next_template)
168
+ await WebsSDK.AuthManager.auto_login(cfg_sessionid, websiteData, requestUrl);
169
+
170
+ // Baja/cancelación
171
+ await WebsSDK.AuthManager.unsubscribeUser({ user_id, callback_url?, org? });
172
+
173
+ // Gestión de perfil
174
+ await WebsSDK.AuthManager.setUserMetadata(params);
175
+ await WebsSDK.AuthManager.uploadProfilePhoto(params); // JPG/PNG/WEBP, max 5MB
176
+ await WebsSDK.AuthManager.changePassword(params);
177
+ ```
178
+
179
+ ### Andromeda — Portales
180
+
181
+ ```typescript
182
+ // Metadata del portal (optimizado con cookie websiteDataFlag)
183
+ await WebsSDK.Andromeda.getWebsiteMetadatabyHostname('portal.com');
184
+
185
+ // Configuración del producto (langs, login_type, logo, favicon, theme)
186
+ await WebsSDK.Andromeda.getWebsiteConfig('portal-corp-web');
187
+ ```
188
+
189
+ ### ContentManager
190
+
191
+ ```typescript
192
+ // Contenido por preset
193
+ await WebsSDK.ContentManager.getContentByPreset('homepage', { lang: 'es' });
194
+
195
+ // Colección filtrada por tags
196
+ await WebsSDK.ContentManager.getCollection(['tag1', 'tag2'], 'es', false);
197
+ ```
198
+
199
+ ### User
200
+
201
+ ```typescript
202
+ await WebsSDK.User.checkSession(token); // Valida token (usado en middleware)
203
+ await WebsSDK.User.getUserData(user_id, website_id);
204
+ await WebsSDK.User.getCredits(user_id, website_id);
205
+ ```
206
+
207
+ ### CorporateAuthManager
208
+
209
+ ```typescript
210
+ // Server-side (middleware Next.js)
211
+ const result = await WebsSDK.CorporateAuthManager.validateCorporateAuthServerSide(request);
212
+ if (!result.authenticated) return result.redirectResponse;
213
+
214
+ // Client-side
215
+ await WebsSDK.CorporateAuthManager.checkCorporateLogin();
216
+ await WebsSDK.CorporateAuthManager.validateUser(autoRedirect?);
217
+ await WebsSDK.CorporateAuthManager.login(returnTo?);
218
+ await WebsSDK.CorporateAuthManager.logout(redirectTo?);
219
+ await WebsSDK.CorporateAuthManager.isAdmin(userData?);
220
+ ```
221
+
222
+ ### Legal
223
+
224
+ ```typescript
225
+ await WebsSDK.Legal.getLegalText('tos' | 'privacy' | 'faq', web_id, lang);
226
+ ```
227
+
228
+ ### Namespace `pre` — Pre-producción
229
+
230
+ ```typescript
231
+ // Lazy-loaded, apunta a bc1742-pre.gways.org
232
+ await WebsSDK.pre.AuthManager.authUser(params);
233
+ await WebsSDK.pre.Andromeda.getWebsiteMetadatabyHostname(hostname);
234
+ ```
235
+
236
+ ---
237
+
238
+ ## Componentes React
239
+
240
+ ### CookieConsent — Banner GDPR
241
+
242
+ ```typescript
243
+ import { CookieConsent, getCookieConsentTexts } from 'webs-sdk';
244
+ import type { CookieConsentProps, CookieConsentTheme, CookieConsentPreferences } from 'webs-sdk';
245
+
246
+ <CookieConsent
247
+ locale="es" // en | es | fr | nl | ar | de | fi | ro | tr
248
+ privacyPolicyUrl="/privacy"
249
+ theme={{
250
+ backgroundColor: '#1a1a2e',
251
+ accentColor: '#6c63ff', // aplica a botón Accept, borde Reject, links y glow
252
+ acceptButtonBg: 'linear-gradient(90deg, #6c63ff, #a855f7)', // soporta gradients CSS
253
+ textColor: '#ffffff',
254
+ }}
255
+ onAccept={() => updateGAConsent('granted')}
256
+ onReject={() => updateGAConsent('denied')}
257
+ onSaveSettings={(prefs: CookieConsentPreferences) => handlePrefs(prefs)}
258
+ />
259
+ ```
260
+
261
+ **Storage:**
262
+ - Cookie `cookieConsent`: `accepted | rejected | custom` (1 año)
263
+ - `localStorage.TRACKING_PERMISSION`: `granted | denied`
264
+ - `localStorage.FUNCTIONAL_PERMISSION`: `granted | denied`
265
+
266
+ ### Google Analytics — GA4
267
+
268
+ ```typescript
269
+ import { initGoogleAnalytics, updateGAConsent, trackGAEvent, trackGAPageView, isGALoaded } from 'webs-sdk';
270
+
271
+ // Registra el GA ID. Carga el script SOLO si hay consentimiento previo.
272
+ await initGoogleAnalytics('G-XXXXXXXXXX', { sendPageView: true, debug: false });
273
+
274
+ // Llamar tras decisión del usuario en CookieConsent
275
+ updateGAConsent('granted'); // carga GA si había pendingTrackingId
276
+ updateGAConsent('denied'); // GA nunca se carga
277
+
278
+ // Tracking
279
+ trackGAEvent('purchase', { value: 9.99, currency: 'EUR' });
280
+ trackGAPageView('/home', 'Inicio');
281
+ isGALoaded(); // boolean
282
+ ```
283
+
284
+ > **GDPR estricto:** GA no usa Consent Mode v2. Si no hay consentimiento, el script `gtag.js` nunca se inyecta en el DOM.
285
+
286
+ ### CDN Proxy
287
+
288
+ ```typescript
289
+ import { handleCDNRequest } from 'webs-sdk';
290
+ import type { CDNOptions } from 'webs-sdk';
291
+
292
+ // Usar en un route handler de Next.js (compatible con Edge Runtime)
293
+ async function handleCDNRequest(
294
+ pathname: string, // '/cdn/apariencias/33702/image.png'
295
+ options?: {
296
+ cdnBaseUrl?: string; // Default: 'https://dy822md8ge77v.cloudfront.net'
297
+ cacheSeconds?: number; // Default: 300 (5 min)
298
+ }
299
+ ): Promise<Response | null> // null si pathname no empieza por /cdn/
300
+ ```
301
+
302
+ ### Componentes "For You" — Creación de contenido AI
303
+
304
+ ```typescript
305
+ import {
306
+ AvatarAI, AvatarAIPage, AvatarAIForYouCard,
307
+ CreativeFaceSwap, CreativeFaceSwapPage, CreativeFaceSwapForYouCard,
308
+ MemeGenerator, MemeGeneratorPage, MemeGeneratorForYouCard,
309
+ WallpapersName, WallpapersNamePage, WallpapersNameForYouCard,
310
+ Wallpapers,
311
+ Ringtone,
312
+ } from 'webs-sdk';
313
+ ```
314
+
315
+ Todos aceptan `websiteId?: string` para tracking multi-portal.
316
+
317
+ ### ARFilters — Filtros AR con DeepAR
318
+
319
+ ```typescript
320
+ import { ARFilters, ARFiltersForYouCard } from 'webs-sdk';
321
+ import type { DeepARProps, DeepARFilter, DeepARTexts } from 'webs-sdk';
322
+
323
+ <ARFilters
324
+ licenseKey="deepar-license-key"
325
+ websiteId="portal-web-id"
326
+ apiBaseUrl="https://api.fluver-ai.com"
327
+ downloadProxyUrl="/cdn" // Proxy CORS para descarga de efectos
328
+ filterTag="ar-filters-tag" // Obtiene filtros del CMS por tag
329
+ // O bien: filters={[{ file: 'beauty', thumbUrl: '/thumb.jpg' }]}
330
+ locale="es"
331
+ texts={getARFiltersTexts('es')} // requerido
332
+ onPhotoTaken={(url) => share(url)}
333
+ onVideoRecorded={(url) => share(url)}
334
+ />
335
+ ```
336
+
337
+ Soporta fullscreen en mobile (vendor-prefixed API para iOS Safari).
338
+
339
+ ### SpinningWheel — Ruleta
340
+
341
+ ```typescript
342
+ import { SpinningWheel, wheelThemes } from 'webs-sdk';
343
+
344
+ // Temas: vodacom | totalgym | womantoday | afristream | efc
345
+ <SpinningWheel
346
+ userId={userId}
347
+ websiteId={websiteId}
348
+ theme="vodacom"
349
+ onResult={(segment) => handleWin(segment)}
350
+ canSpin={true}
351
+ alreadySpunToday={false}
352
+ />
353
+ ```
354
+
355
+ ### Quiz y Stickers
356
+
357
+ ```typescript
358
+ import { Quiz, Stickers } from 'webs-sdk';
359
+ ```
360
+
361
+ ### Esports — Noticias, Vídeos y Partidas en Directo
362
+
363
+ ```typescript
364
+ import { EsportsNews, EsportsVideos, EsportsLive } from 'webs-sdk';
365
+ ```
366
+
367
+ #### EsportsNews
368
+
369
+ ```typescript
370
+ <EsportsNews
371
+ config={{
372
+ key: 'api-key',
373
+ contents: [{ preset: 'esports_news', apiProviderId: 'id', category: [], tags: [], limit: 10 }]
374
+ }}
375
+ language="es"
376
+ onNewsClick={(item) => router.push(`/news/${item.id}`)}
377
+ theme={theme} // CSS classes para container, cards, badges, etc.
378
+ categoryStyles={{ gaming: { badge: 'bg-blue-500', badgeText: 'text-white' } }}
379
+ // Modo detalle (renderiza EsportsNewsDetail):
380
+ newsId="article-id"
381
+ newsData={item} // opcional si ya tienes los datos
382
+ onBack={() => router.back()}
383
+ />
384
+ ```
385
+
386
+ #### EsportsVideos
387
+
388
+ ```typescript
389
+ <EsportsVideos
390
+ config={{ key: 'api-key', contents: [...] }}
391
+ language="es"
392
+ onVideoClick={(item) => openPlayer(item.video_url)}
393
+ theme={theme} // incluye clases para duration overlay
394
+ />
395
+ ```
396
+
397
+ #### EsportsLive
398
+
399
+ ```typescript
400
+ <EsportsLive
401
+ language="es"
402
+ timezone="Europe/Madrid"
403
+ onLiveClick={(item) => openStream(item.streamUrl)}
404
+ onMatchClick={(match) => openMatchDetail(match)}
405
+ onGameClick={(gameId) => filterByGame(gameId)}
406
+ website_id="portal-web-id"
407
+ theme={theme}
408
+ // Modo detalle de partida:
409
+ matchId="serie-id"
410
+ matchData={calendarMatch}
411
+ onBack={() => setMatchId(null)}
412
+ />
413
+ ```
414
+
415
+ ---
416
+
417
+ ## Utilidades de validación de vídeo
418
+
419
+ ```typescript
420
+ import { validateVideoUrl, validateVideoBatch, clearValidationCache } from 'webs-sdk';
421
+ import { useValidateVideoOnMount, useLazyVideoValidation } from 'webs-sdk';
422
+
423
+ // Función pura (sin React)
424
+ const state = await validateVideoUrl('https://stream.example.com/live.m3u8');
425
+ // 'valid' | 'invalid' | 'unknown'
426
+
427
+ // Batch con concurrencia 3
428
+ const results = await validateVideoBatch([url1, url2, url3]);
429
+
430
+ // React: validar al montar (evita renderizar streams rotos)
431
+ const { isValid, isInvalid, isValidating } = useValidateVideoOnMount(videoUrl);
432
+
433
+ // React: validación manual
434
+ const { validate, isValid, isInvalid } = useLazyVideoValidation();
435
+ await validate(videoUrl);
436
+ ```
437
+
438
+ Soporta HLS, DASH, MP4. Cache en memoria con TTL de 5 minutos.
439
+
440
+ ---
441
+
442
+ ## Estructura del proyecto
443
+
444
+ ```
445
+ src/
446
+ ├── config.ts # Configuración central (URLs, endpoints, constantes)
447
+ ├── index.ts # Barrel export principal
448
+ ├── libraries/
449
+ │ ├── networking.ts # HTTP + events
450
+ │ ├── auth.ts # Autenticación, login, baja
451
+ │ ├── session.ts # Sesiones y dispositivo
452
+ │ ├── storage.ts # LocalStorage + compresión de imágenes
453
+ │ ├── utils.ts # Utilidades generales
454
+ │ ├── andromeda.ts # Metadata y config de portales
455
+ │ ├── corp_auth.ts # Auth corporativa (JWT)
456
+ │ ├── user.ts # Datos de usuario
457
+ │ ├── mixpanel.ts # Analytics
458
+ │ ├── audio.ts # Text-to-speech
459
+ │ ├── calypso.ts # Event tracking interno
460
+ │ ├── legal.ts # Textos legales
461
+ │ ├── content.ts # Contenido dinámico + colecciones por tags
462
+ │ ├── cdn.ts # CDN Proxy (Edge Runtime compatible)
463
+ │ └── googleAnalytics.ts # GA4 con carga diferida por consentimiento
464
+ ├── utils/
465
+ │ ├── videoValidation.ts # Validación de URLs de streaming
466
+ │ ├── useVideoValidation.ts # React hooks para validación
467
+ │ └── timeUtils.ts # getTimeAgo — tiempo relativo i18n
468
+ └── components/
469
+ ├── AvatarAI.tsx
470
+ ├── CreativeFaceSwap.tsx
471
+ ├── Wallpapers.tsx
472
+ ├── WallpapersName.tsx
473
+ ├── Ringtone.tsx
474
+ ├── MemeGenerator.tsx
475
+ ├── Quiz.tsx
476
+ ├── Stickers.tsx
477
+ ├── VideoPlayer.tsx
478
+ ├── ARFilters/
479
+ ├── SpinningWheel/
480
+ ├── CookieConsent/
481
+ └── esports/
482
+ ├── news/
483
+ ├── videos/
484
+ └── live/
485
+ ```
486
+
487
+ ## Desarrollo
488
+
489
+ ```bash
490
+ npm install
491
+ npm run build # tsc → dist/
492
+ npm run build:watch # watch mode
493
+ npm run lint
494
+ ```
495
+
496
+ ## Publicación
497
+
498
+ ```bash
499
+ # Incluir [ROAD-361723] en el commit para auto-publish en CI
500
+ git commit -m "feat: descripción [ROAD-361723]"
501
+ git push
502
+ ```
503
+
504
+ Tras publicar, actualizar en los portales:
505
+ ```bash
506
+ npm install webs-sdk@<nueva-version>
507
+ ```
508
+
509
+ **Nota importante — `index.js` raíz:** es el barrel CommonJS mantenido manualmente. Cada export nuevo en `src/index.ts` debe añadirse también en `index.js`:
510
+ ```js
511
+ const { nuevaFuncion } = require('./dist/index.js');
512
+ module.exports.nuevaFuncion = nuevaFuncion;
513
+ ```