@utilia-os/sdk-js 1.4.0 → 1.7.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/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/client.ts
2
- import axios, { AxiosError } from "axios";
2
+ import axios2, { AxiosError } from "axios";
3
3
 
4
4
  // src/errors/sdk-error.ts
5
5
  var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
@@ -9,6 +9,7 @@ var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
9
9
  ErrorCode2["RATE_LIMITED"] = "RATE_LIMITED";
10
10
  ErrorCode2["VALIDATION_ERROR"] = "VALIDATION_ERROR";
11
11
  ErrorCode2["NETWORK_ERROR"] = "NETWORK_ERROR";
12
+ ErrorCode2["AUTH_EXPIRED"] = "AUTH_EXPIRED";
12
13
  ErrorCode2["UNKNOWN"] = "UNKNOWN";
13
14
  return ErrorCode2;
14
15
  })(ErrorCode || {});
@@ -59,23 +60,469 @@ var UtiliaSDKError = class _UtiliaSDKError extends Error {
59
60
  }
60
61
  };
61
62
 
63
+ // src/auth/oauth.service.ts
64
+ import axios from "axios";
65
+
66
+ // src/auth/pkce.ts
67
+ function base64UrlEncode(buffer) {
68
+ const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
69
+ let binary = "";
70
+ for (let i = 0; i < bytes.length; i++) {
71
+ binary += String.fromCharCode(bytes[i]);
72
+ }
73
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
74
+ }
75
+ function generateCodeVerifier(length = 64) {
76
+ const validLength = Math.max(43, Math.min(128, length));
77
+ const bytes = new Uint8Array(validLength);
78
+ crypto.getRandomValues(bytes);
79
+ return base64UrlEncode(bytes).slice(0, validLength);
80
+ }
81
+ async function generateCodeChallenge(verifier) {
82
+ const encoder = new TextEncoder();
83
+ const data = encoder.encode(verifier);
84
+ const digest = await crypto.subtle.digest("SHA-256", data);
85
+ return base64UrlEncode(digest);
86
+ }
87
+
88
+ // src/auth/token-storage.ts
89
+ var MemoryTokenStorage = class {
90
+ constructor() {
91
+ this.tokens = null;
92
+ this.pendingState = null;
93
+ }
94
+ async getTokens() {
95
+ return this.tokens;
96
+ }
97
+ async setTokens(tokens) {
98
+ this.tokens = tokens;
99
+ }
100
+ async clearTokens() {
101
+ this.tokens = null;
102
+ }
103
+ async getPendingState() {
104
+ return this.pendingState;
105
+ }
106
+ async setPendingState(state) {
107
+ this.pendingState = state;
108
+ }
109
+ async clearPendingState() {
110
+ this.pendingState = null;
111
+ }
112
+ };
113
+ var FileTokenStorage = class {
114
+ /**
115
+ * @param filePath - Ruta absoluta al archivo donde se guardarán los tokens
116
+ */
117
+ constructor(filePath) {
118
+ this.filePath = filePath;
119
+ }
120
+ async getTokens() {
121
+ try {
122
+ const fs = await import("fs/promises");
123
+ const content = await fs.readFile(this.filePath, "utf-8");
124
+ const data = JSON.parse(content);
125
+ if (data._pendingState) {
126
+ return null;
127
+ }
128
+ return data;
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
133
+ async setTokens(tokens) {
134
+ const fs = await import("fs/promises");
135
+ await fs.writeFile(this.filePath, JSON.stringify(tokens, null, 2), { encoding: "utf-8", mode: 384 });
136
+ }
137
+ async clearTokens() {
138
+ try {
139
+ const fs = await import("fs/promises");
140
+ await fs.unlink(this.filePath);
141
+ } catch {
142
+ }
143
+ }
144
+ get pendingStatePath() {
145
+ return this.filePath.replace(/\.json$/, "") + ".pending.json";
146
+ }
147
+ async getPendingState() {
148
+ try {
149
+ const fs = await import("fs/promises");
150
+ const content = await fs.readFile(this.pendingStatePath, "utf-8");
151
+ return JSON.parse(content);
152
+ } catch {
153
+ return null;
154
+ }
155
+ }
156
+ async setPendingState(state) {
157
+ const fs = await import("fs/promises");
158
+ await fs.writeFile(this.pendingStatePath, JSON.stringify(state, null, 2), { encoding: "utf-8", mode: 384 });
159
+ }
160
+ async clearPendingState() {
161
+ try {
162
+ const fs = await import("fs/promises");
163
+ await fs.unlink(this.pendingStatePath);
164
+ } catch {
165
+ }
166
+ }
167
+ };
168
+
169
+ // src/auth/oauth.service.ts
170
+ var OAuthService = class {
171
+ constructor(baseURL, config) {
172
+ /** Estado PKCE en memoria como fallback cuando el storage no soporta pendingState */
173
+ this._pendingState = null;
174
+ this.baseURL = baseURL.replace(/\/$/, "");
175
+ this.config = config;
176
+ this.storage = config.tokenStorage ?? new MemoryTokenStorage();
177
+ this.http = axios.create({
178
+ baseURL: this.baseURL,
179
+ timeout: 3e4,
180
+ headers: { "Content-Type": "application/json" }
181
+ });
182
+ }
183
+ /**
184
+ * Genera la URL de autorización OAuth con PKCE y almacena el estado
185
+ * pendiente automáticamente. Usar con handleCallback(code) sin codeVerifier.
186
+ *
187
+ * Este es el método recomendado para la mayoría de los casos.
188
+ *
189
+ * @param options - Opciones adicionales (state personalizado, scopes)
190
+ * @returns URL de autorización lista para redirigir al usuario
191
+ */
192
+ async getAuthorizationUrl(options) {
193
+ const result = await this.getLoginUrl(options);
194
+ const pending = {
195
+ codeVerifier: result.codeVerifier,
196
+ state: result.state
197
+ };
198
+ this._pendingState = pending;
199
+ if (this.storage.setPendingState) {
200
+ await this.storage.setPendingState(pending);
201
+ }
202
+ return result.url;
203
+ }
204
+ /**
205
+ * Genera la URL de inicio de sesión OAuth con PKCE.
206
+ *
207
+ * Método de control manual: devuelve codeVerifier y state que el desarrollador
208
+ * debe gestionar. Para un flujo automático, usar getAuthorizationUrl() en su lugar.
209
+ *
210
+ * @param options - Opciones adicionales (state personalizado, scopes)
211
+ * @returns URL de autorización, code_verifier y state
212
+ */
213
+ async getLoginUrl(options) {
214
+ const codeVerifier = generateCodeVerifier();
215
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
216
+ const state = options?.state ?? Array.from(crypto.getRandomValues(new Uint8Array(24)), (b) => b.toString(16).padStart(2, "0")).join("");
217
+ const scopes = options?.scopes ?? this.config.scopes ?? ["openid", "profile", "email"];
218
+ const params = new URLSearchParams({
219
+ response_type: "code",
220
+ client_id: this.config.clientId,
221
+ redirect_uri: this.config.redirectUri,
222
+ scope: scopes.join(" "),
223
+ state,
224
+ code_challenge: codeChallenge,
225
+ code_challenge_method: "S256"
226
+ });
227
+ return {
228
+ url: `${this.baseURL}/oauth/authorize?${params.toString()}`,
229
+ codeVerifier,
230
+ state
231
+ };
232
+ }
233
+ /**
234
+ * Abre una ventana emergente para inicio de sesión OAuth y devuelve tokens al completarse.
235
+ *
236
+ * @param options - Opciones de configuración de la ventana emergente
237
+ * @returns Tokens OAuth
238
+ * @throws Error si la ventana es bloqueada, cerrada o excede el tiempo límite
239
+ */
240
+ async loginWithPopup(options) {
241
+ if (typeof window === "undefined") {
242
+ throw new Error("loginWithPopup solo est\xE1 disponible en entornos de navegador.");
243
+ }
244
+ const { url, codeVerifier, state } = await this.getLoginUrl({
245
+ state: options?.state,
246
+ scopes: options?.scopes
247
+ });
248
+ const width = options?.width ?? 500;
249
+ const height = options?.height ?? 700;
250
+ const timeout = options?.timeout ?? 3e5;
251
+ const left = Math.max(0, Math.round(window.screenX + (window.outerWidth - width) / 2));
252
+ const top = Math.max(0, Math.round(window.screenY + (window.outerHeight - height) / 2));
253
+ const features = `width=${width},height=${height},left=${left},top=${top},scrollbars=yes,resizable=yes,status=yes`;
254
+ const popup = window.open(url, "utilia-oauth-popup", features);
255
+ if (!popup || popup.closed) {
256
+ throw new Error(
257
+ "No se pudo abrir la ventana de autorizaci\xF3n. Verifica que los popups no est\xE9n bloqueados."
258
+ );
259
+ }
260
+ return new Promise((resolve, reject) => {
261
+ let timeoutId;
262
+ let pollId;
263
+ const cleanup = () => {
264
+ clearTimeout(timeoutId);
265
+ clearInterval(pollId);
266
+ window.removeEventListener("message", onMessage);
267
+ };
268
+ const onMessage = async (event) => {
269
+ if (event.origin !== window.location.origin) return;
270
+ const data = event.data;
271
+ if (!data || typeof data !== "object") return;
272
+ if (data.type === "oauth-mcp-success" && data.state === state) {
273
+ cleanup();
274
+ try {
275
+ if (popup && !popup.closed) popup.close();
276
+ } catch {
277
+ }
278
+ try {
279
+ const tokens = await this.handleCallback(data.code, codeVerifier);
280
+ resolve(tokens);
281
+ } catch (err) {
282
+ reject(err);
283
+ }
284
+ } else if (data.type === "oauth-mcp-error" && data.state === state) {
285
+ cleanup();
286
+ try {
287
+ if (popup && !popup.closed) popup.close();
288
+ } catch {
289
+ }
290
+ reject(new Error(data.error_description || data.error || "Autorizaci\xF3n denegada"));
291
+ }
292
+ };
293
+ window.addEventListener("message", onMessage);
294
+ pollId = setInterval(() => {
295
+ if (popup.closed) {
296
+ cleanup();
297
+ reject(new Error("La ventana de autorizaci\xF3n fue cerrada antes de completar el proceso."));
298
+ }
299
+ }, 500);
300
+ timeoutId = setTimeout(() => {
301
+ cleanup();
302
+ try {
303
+ if (popup && !popup.closed) popup.close();
304
+ } catch {
305
+ }
306
+ reject(new Error("El proceso de autorizaci\xF3n excedi\xF3 el tiempo l\xEDmite."));
307
+ }, timeout);
308
+ });
309
+ }
310
+ /**
311
+ * Intercambia el código de autorización por tokens.
312
+ *
313
+ * Si se llama sin codeVerifier, recupera automáticamente el estado PKCE
314
+ * almacenado por getAuthorizationUrl() y valida el state CSRF.
315
+ *
316
+ * @param code - Código de autorización recibido en el callback
317
+ * @param codeVerifier - Verificador PKCE (opcional si se usó getAuthorizationUrl)
318
+ * @param callbackState - Valor de state recibido en el callback (para validación CSRF automática)
319
+ * @returns Tokens OAuth
320
+ */
321
+ async handleCallback(code, codeVerifier, callbackState) {
322
+ let verifier = codeVerifier;
323
+ if (!verifier) {
324
+ const pending = await this.getPendingState();
325
+ if (!pending) {
326
+ throw new Error(
327
+ "No se encontr\xF3 el estado PKCE pendiente. Usa getAuthorizationUrl() antes de handleCallback(), o proporciona el codeVerifier manualmente."
328
+ );
329
+ }
330
+ verifier = pending.codeVerifier;
331
+ if (callbackState && callbackState !== pending.state) {
332
+ await this.clearPendingState();
333
+ throw new Error(
334
+ `El valor de state no coincide. Posible ataque CSRF. Esperado: ${pending.state}, recibido: ${callbackState}`
335
+ );
336
+ }
337
+ await this.clearPendingState();
338
+ }
339
+ const body = {
340
+ grant_type: "authorization_code",
341
+ code,
342
+ redirect_uri: this.config.redirectUri,
343
+ code_verifier: verifier,
344
+ client_id: this.config.clientId
345
+ };
346
+ if (this.config.clientSecret) {
347
+ body.client_secret = this.config.clientSecret;
348
+ }
349
+ const response = await this.http.post("/oauth/token", body);
350
+ const data = response.data;
351
+ const tokens = {
352
+ accessToken: data.access_token,
353
+ refreshToken: data.refresh_token,
354
+ expiresIn: data.expires_in,
355
+ expiresAt: Date.now() + data.expires_in * 1e3,
356
+ tokenType: data.token_type || "Bearer",
357
+ scope: data.scope,
358
+ idToken: data.id_token
359
+ };
360
+ await this.storage.setTokens(tokens);
361
+ return tokens;
362
+ }
363
+ /**
364
+ * Refresca el token de acceso usando el refresh_token
365
+ *
366
+ * @returns Nuevos tokens OAuth
367
+ * @throws Error si no hay refresh_token disponible
368
+ */
369
+ async refreshToken() {
370
+ const current = await this.storage.getTokens();
371
+ if (!current?.refreshToken) {
372
+ throw new Error("No hay refresh token disponible. El usuario debe volver a autenticarse.");
373
+ }
374
+ const body = {
375
+ grant_type: "refresh_token",
376
+ refresh_token: current.refreshToken,
377
+ client_id: this.config.clientId
378
+ };
379
+ if (this.config.clientSecret) {
380
+ body.client_secret = this.config.clientSecret;
381
+ }
382
+ const response = await this.http.post("/oauth/token", body);
383
+ const data = response.data;
384
+ const tokens = {
385
+ accessToken: data.access_token,
386
+ refreshToken: data.refresh_token ?? current.refreshToken,
387
+ expiresIn: data.expires_in,
388
+ expiresAt: Date.now() + data.expires_in * 1e3,
389
+ tokenType: data.token_type || "Bearer",
390
+ scope: data.scope,
391
+ idToken: data.id_token
392
+ };
393
+ await this.storage.setTokens(tokens);
394
+ return tokens;
395
+ }
396
+ /**
397
+ * Obtiene información del usuario autenticado
398
+ *
399
+ * @returns Datos del usuario desde /oauth/userinfo
400
+ */
401
+ async getUserInfo() {
402
+ const accessToken = await this.getAccessToken();
403
+ const response = await this.http.get("/oauth/userinfo", {
404
+ headers: { Authorization: `Bearer ${accessToken}` }
405
+ });
406
+ return response.data;
407
+ }
408
+ /**
409
+ * Revoca el token actual
410
+ *
411
+ * @param tokenType - Tipo de token a revocar ('access_token' | 'refresh_token').
412
+ * Por defecto revoca el access_token.
413
+ */
414
+ async revokeToken(tokenType) {
415
+ const current = await this.storage.getTokens();
416
+ if (!current) return;
417
+ const token = tokenType === "refresh_token" ? current.refreshToken : current.accessToken;
418
+ if (!token) return;
419
+ const body = {
420
+ token,
421
+ client_id: this.config.clientId
422
+ };
423
+ if (tokenType) {
424
+ body.token_type_hint = tokenType;
425
+ }
426
+ if (this.config.clientSecret) {
427
+ body.client_secret = this.config.clientSecret;
428
+ }
429
+ try {
430
+ await this.http.post("/oauth/revoke", body);
431
+ } catch {
432
+ }
433
+ await this.storage.clearTokens();
434
+ }
435
+ /**
436
+ * Obtiene un access token válido, refrescándolo automáticamente si ha expirado
437
+ *
438
+ * @returns Access token válido
439
+ * @throws Error si no hay tokens disponibles
440
+ */
441
+ async getAccessToken() {
442
+ const tokens = await this.storage.getTokens();
443
+ if (!tokens) {
444
+ throw new Error("No hay tokens OAuth. El usuario debe iniciar sesi\xF3n primero.");
445
+ }
446
+ if (tokens.expiresAt < Date.now() + 3e4) {
447
+ const refreshed = await this.refreshToken();
448
+ return refreshed.accessToken;
449
+ }
450
+ return tokens.accessToken;
451
+ }
452
+ /**
453
+ * Verifica si hay una sesión OAuth activa con tokens válidos.
454
+ *
455
+ * @returns true si hay tokens almacenados y el access token no ha expirado
456
+ * (o si hay un refresh token disponible para renovarlo)
457
+ */
458
+ async isAuthenticated() {
459
+ const tokens = await this.storage.getTokens();
460
+ if (!tokens) return false;
461
+ if (tokens.expiresAt > Date.now() + 3e4) return true;
462
+ return !!tokens.refreshToken;
463
+ }
464
+ /**
465
+ * Cierra la sesión OAuth: revoca los tokens y limpia el almacenamiento.
466
+ */
467
+ async logout() {
468
+ await this.revokeToken();
469
+ }
470
+ // -- Helpers internos para estado PKCE pendiente --
471
+ async getPendingState() {
472
+ if (this.storage.getPendingState) {
473
+ const stored = await this.storage.getPendingState();
474
+ if (stored) return stored;
475
+ }
476
+ return this._pendingState;
477
+ }
478
+ async clearPendingState() {
479
+ this._pendingState = null;
480
+ if (this.storage.clearPendingState) {
481
+ await this.storage.clearPendingState();
482
+ }
483
+ }
484
+ };
485
+
62
486
  // src/client.ts
63
487
  var UtiliaClient = class {
64
488
  constructor(config) {
489
+ /** Promesa compartida para evitar refreshes concurrentes */
490
+ this._refreshPromise = null;
491
+ if (!config.apiKey && !config.oauth) {
492
+ throw new UtiliaSDKError(
493
+ "VALIDATION_ERROR" /* VALIDATION_ERROR */,
494
+ "Debes proporcionar apiKey o configuraci\xF3n oauth"
495
+ );
496
+ }
497
+ if (config.apiKey && config.oauth) {
498
+ throw new UtiliaSDKError(
499
+ "VALIDATION_ERROR" /* VALIDATION_ERROR */,
500
+ "No puedes proporcionar apiKey y oauth a la vez. Usa uno u otro."
501
+ );
502
+ }
65
503
  this.config = {
66
504
  timeout: 3e4,
67
505
  retryAttempts: 3,
68
506
  debug: false,
69
507
  ...config
70
508
  };
71
- this.axios = axios.create({
509
+ const headers = {
510
+ "Content-Type": "application/json"
511
+ };
512
+ if (this.config.apiKey) {
513
+ headers["X-Api-Key"] = this.config.apiKey;
514
+ }
515
+ this.axios = axios2.create({
72
516
  baseURL: this.config.baseURL,
73
517
  timeout: this.config.timeout,
74
- headers: {
75
- "Content-Type": "application/json",
76
- "X-Api-Key": this.config.apiKey
77
- }
518
+ headers
78
519
  });
520
+ if (this.config.oauth) {
521
+ this._oauthService = new OAuthService(this.config.baseURL, this.config.oauth);
522
+ if (this.config.oauth.tokenStorage) {
523
+ }
524
+ this.setupOAuthInterceptors();
525
+ }
79
526
  this.axios.interceptors.request.use((config2) => {
80
527
  const requestId = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : this.generateSimpleId();
81
528
  config2.headers["x-request-id"] = requestId;
@@ -84,20 +531,71 @@ var UtiliaClient = class {
84
531
  });
85
532
  this.setupInterceptors();
86
533
  }
534
+ /**
535
+ * Acceso al servicio OAuth (solo disponible en modo OAuth)
536
+ */
537
+ get oauth() {
538
+ if (!this._oauthService) {
539
+ throw new UtiliaSDKError(
540
+ "VALIDATION_ERROR" /* VALIDATION_ERROR */,
541
+ "El servicio OAuth no est\xE1 disponible. Configura oauth en el constructor del SDK."
542
+ );
543
+ }
544
+ return this._oauthService;
545
+ }
546
+ /**
547
+ * Configura interceptores para autenticación OAuth
548
+ */
549
+ setupOAuthInterceptors() {
550
+ this.axios.interceptors.request.use(async (config) => {
551
+ if (this._oauthService) {
552
+ try {
553
+ const accessToken = await this._oauthService.getAccessToken();
554
+ config.headers["Authorization"] = `Bearer ${accessToken}`;
555
+ } catch {
556
+ }
557
+ }
558
+ return config;
559
+ });
560
+ this.axios.interceptors.response.use(
561
+ (response) => response,
562
+ async (error) => {
563
+ const originalRequest = error.config;
564
+ if (error.response?.status === 401 && this._oauthService && originalRequest && !originalRequest._retried) {
565
+ originalRequest._retried = true;
566
+ if (!this._refreshPromise) {
567
+ this._refreshPromise = this._oauthService.refreshToken().then(() => {
568
+ }).finally(() => {
569
+ this._refreshPromise = null;
570
+ });
571
+ }
572
+ try {
573
+ await this._refreshPromise;
574
+ const newToken = await this._oauthService.getAccessToken();
575
+ originalRequest.headers["Authorization"] = `Bearer ${newToken}`;
576
+ return this.axios(originalRequest);
577
+ } catch {
578
+ throw error;
579
+ }
580
+ }
581
+ throw error;
582
+ }
583
+ );
584
+ }
87
585
  /**
88
586
  * Configura los interceptores de request y response
89
587
  */
90
588
  setupInterceptors() {
91
589
  if (this.config.debug) {
92
590
  this.axios.interceptors.request.use((request) => {
93
- console.log("[UtiliaSDK] Request:", request.method?.toUpperCase(), request.url);
591
+ console.log("[UtiliaSDK] Petici\xF3n:", request.method?.toUpperCase(), request.url);
94
592
  return request;
95
593
  });
96
594
  }
97
595
  if (this.config.debug) {
98
596
  this.axios.interceptors.response.use(
99
597
  (response) => {
100
- console.log("[UtiliaSDK] Response:", response.status, response.config.url);
598
+ console.log("[UtiliaSDK] Respuesta:", response.status, response.config.url);
101
599
  return response;
102
600
  }
103
601
  );
@@ -114,15 +612,15 @@ var UtiliaClient = class {
114
612
  const errorOptions = { requestId, statusCode, errorCode };
115
613
  if (!error.response) {
116
614
  if (error.code === "ECONNABORTED") {
117
- return new UtiliaSDKError("NETWORK_ERROR" /* NETWORK_ERROR */, "Timeout: la solicitud excedio el tiempo limite", errorOptions);
615
+ return new UtiliaSDKError("NETWORK_ERROR" /* NETWORK_ERROR */, "Timeout: la solicitud excedi\xF3 el tiempo l\xEDmite", errorOptions);
118
616
  }
119
- return new UtiliaSDKError("NETWORK_ERROR" /* NETWORK_ERROR */, "Error de conexion: no se pudo conectar con el servidor", errorOptions);
617
+ return new UtiliaSDKError("NETWORK_ERROR" /* NETWORK_ERROR */, "Error de conexi\xF3n: no se pudo conectar con el servidor", errorOptions);
120
618
  }
121
619
  const status = error.response.status;
122
620
  const message = data?.message || data?.error || error.message;
123
621
  switch (status) {
124
622
  case 401:
125
- return new UtiliaSDKError("UNAUTHORIZED" /* UNAUTHORIZED */, message || "API Key invalida o faltante", errorOptions);
623
+ return new UtiliaSDKError("UNAUTHORIZED" /* UNAUTHORIZED */, message || "API Key inv\xE1lida o faltante", errorOptions);
126
624
  case 403:
127
625
  if (message?.toLowerCase().includes("rate limit")) {
128
626
  return new UtiliaSDKError("RATE_LIMITED" /* RATE_LIMITED */, "Rate limit excedido. Intenta de nuevo mas tarde.", errorOptions);
@@ -135,7 +633,7 @@ var UtiliaClient = class {
135
633
  return new UtiliaSDKError("NOT_FOUND" /* NOT_FOUND */, message || "Recurso no encontrado", errorOptions);
136
634
  case 400:
137
635
  case 422:
138
- return new UtiliaSDKError("VALIDATION_ERROR" /* VALIDATION_ERROR */, message || "Error de validacion en los datos enviados", errorOptions);
636
+ return new UtiliaSDKError("VALIDATION_ERROR" /* VALIDATION_ERROR */, message || "Error de validaci\xF3n en los datos enviados", errorOptions);
139
637
  case 429:
140
638
  return new UtiliaSDKError("RATE_LIMITED" /* RATE_LIMITED */, message || "Rate limit excedido. Intenta de nuevo mas tarde.", errorOptions);
141
639
  case 500:
@@ -148,7 +646,7 @@ var UtiliaClient = class {
148
646
  }
149
647
  }
150
648
  /**
151
- * Determina si un error es recuperable y se debe reintentar
649
+ * Determina si un error es recuperable y se debe reintentar.
152
650
  */
153
651
  isRetryableError(error) {
154
652
  if (error instanceof AxiosError) {
@@ -162,7 +660,7 @@ var UtiliaClient = class {
162
660
  return false;
163
661
  }
164
662
  /**
165
- * Ejecuta una funcion con reintentos y backoff exponencial
663
+ * Ejecuta una función con reintentos y backoff exponencial
166
664
  */
167
665
  async executeWithRetry(fn) {
168
666
  let lastError;
@@ -180,7 +678,7 @@ var UtiliaClient = class {
180
678
  if (attempt < this.config.retryAttempts - 1) {
181
679
  const delay = 500 * Math.pow(2, attempt);
182
680
  if (this.config.debug) {
183
- console.log(`[UtiliaSDK] Retry attempt ${attempt + 1}/${this.config.retryAttempts - 1}, waiting ${delay}ms`);
681
+ console.log(`[UtiliaSDK] Reintento ${attempt + 1}/${this.config.retryAttempts - 1}, esperando ${delay}ms`);
184
682
  }
185
683
  await this.sleep(delay);
186
684
  }
@@ -192,7 +690,7 @@ var UtiliaClient = class {
192
690
  throw lastError;
193
691
  }
194
692
  /**
195
- * Genera un ID simple como fallback cuando crypto.randomUUID no esta disponible
693
+ * Genera un ID simple como fallback cuando crypto.randomUUID no está disponible
196
694
  */
197
695
  generateSimpleId() {
198
696
  return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
@@ -204,19 +702,44 @@ var UtiliaClient = class {
204
702
  return new Promise((resolve) => setTimeout(resolve, ms));
205
703
  }
206
704
  /**
207
- * Construye una URL completa para conexiones SSE con la API key como query param.
208
- * Util para EventSource que no soporta headers personalizados.
705
+ * Construye una URL completa para conexiones SSE con credenciales como query param.
706
+ * Necesario porque EventSource no soporta headers personalizados.
209
707
  *
210
708
  * @param path - Ruta relativa de la API (ej: /external/v1/tickets/ai-suggest/123/stream)
211
- * @returns URL completa con apiKey como parametro de consulta
709
+ * @returns URL completa con credenciales como parametro de consulta
212
710
  */
213
711
  buildSseUrl(path) {
214
712
  const base = this.config.baseURL.replace(/\/$/, "");
215
713
  const separator = path.includes("?") ? "&" : "?";
216
- return `${base}${path}${separator}apiKey=${this.config.apiKey}`;
714
+ if (this.config.apiKey) {
715
+ return `${base}${path}${separator}apiKey=${this.config.apiKey}`;
716
+ }
717
+ return `${base}${path}`;
718
+ }
719
+ /**
720
+ * Construye una URL completa para conexiones SSE, incluyendo token OAuth si está disponible.
721
+ * Versión asíncrona que obtiene el access token actual.
722
+ *
723
+ * @param path - Ruta relativa de la API
724
+ * @returns URL completa con credenciales como parametro de consulta
725
+ */
726
+ async buildSseUrlAsync(path) {
727
+ const base = this.config.baseURL.replace(/\/$/, "");
728
+ const separator = path.includes("?") ? "&" : "?";
729
+ if (this.config.apiKey) {
730
+ return `${base}${path}${separator}apiKey=${this.config.apiKey}`;
731
+ }
732
+ if (this._oauthService) {
733
+ try {
734
+ const accessToken = await this._oauthService.getAccessToken();
735
+ return `${base}${path}${separator}access_token=${accessToken}`;
736
+ } catch {
737
+ }
738
+ }
739
+ return `${base}${path}`;
217
740
  }
218
741
  /**
219
- * Realiza una peticion GET
742
+ * Realiza una petición GET
220
743
  */
221
744
  async get(url, config) {
222
745
  return this.executeWithRetry(async () => {
@@ -596,6 +1119,54 @@ var TicketsService = class {
596
1119
  { params: { userId } }
597
1120
  );
598
1121
  }
1122
+ /**
1123
+ * Suscribirse a actualizaciones de tickets en tiempo real via SSE.
1124
+ * Recibe eventos cuando un agente responde, cambia el estado, resuelve o cierra un ticket.
1125
+ *
1126
+ * Requiere un entorno con EventSource nativo (navegador) o un polyfill como 'eventsource'.
1127
+ *
1128
+ * @param userId - ID externo del usuario (opcional, filtra eventos por usuario)
1129
+ * @param options - Callbacks para eventos y errores
1130
+ * @returns Objeto con metodo close() para cerrar la conexion
1131
+ *
1132
+ * @example
1133
+ * ```typescript
1134
+ * const stream = sdk.tickets.streamUpdates('user-123', {
1135
+ * onTicketUpdated: (event) => {
1136
+ * console.log(`Ticket ${event.ticketKey} actualizado: ${event.type}`);
1137
+ * // Refrescar la UI del ticket
1138
+ * },
1139
+ * });
1140
+ *
1141
+ * // Para cerrar la conexion:
1142
+ * stream.close();
1143
+ * ```
1144
+ */
1145
+ streamUpdates(userId, options) {
1146
+ if (typeof EventSource === "undefined") {
1147
+ throw new Error(
1148
+ 'EventSource no esta disponible en este entorno. Para Node.js, instala un polyfill como "eventsource" y asignalo a globalThis.EventSource.'
1149
+ );
1150
+ }
1151
+ const queryParams = userId ? `?userId=${encodeURIComponent(userId)}` : "";
1152
+ const sseUrl = this.client.buildSseUrl(`${this.basePath}/events${queryParams}`);
1153
+ const eventSource = new EventSource(sseUrl);
1154
+ eventSource.onmessage = (event) => {
1155
+ try {
1156
+ const data = JSON.parse(event.data);
1157
+ if (data.event === "ticket-updated" && options?.onTicketUpdated) {
1158
+ options.onTicketUpdated(data);
1159
+ }
1160
+ } catch {
1161
+ }
1162
+ };
1163
+ eventSource.onerror = () => {
1164
+ options?.onError?.(new Error("Error en la conexion SSE"));
1165
+ };
1166
+ return {
1167
+ close: () => eventSource.close()
1168
+ };
1169
+ }
599
1170
  /**
600
1171
  * Cerrar un ticket
601
1172
  * Solo el usuario que creo el ticket puede cerrarlo
@@ -1101,39 +1672,57 @@ var UtiliaSDK = class {
1101
1672
  /**
1102
1673
  * Crea una nueva instancia del SDK
1103
1674
  *
1104
- * @param config - Configuracion del SDK
1675
+ * @param config - Configuración del SDK
1105
1676
  *
1106
- * @example
1677
+ * @example Autenticación con API Key
1107
1678
  * ```typescript
1108
1679
  * const sdk = new UtiliaSDK({
1109
1680
  * baseURL: 'https://os.utilia.ai/api',
1110
1681
  * apiKey: 'tu-api-key',
1111
- * timeout: 30000, // opcional
1112
- * debug: true, // opcional, habilita logs
1113
1682
  * });
1683
+ * ```
1114
1684
  *
1115
- * // Subir archivo y crear ticket con adjunto
1116
- * const file = await sdk.files.upload(inputFile);
1117
- * const ticket = await sdk.tickets.create({
1118
- * user: { externalId: 'user-123' },
1119
- * title: 'Problema con facturacion',
1120
- * description: 'No puedo ver mis facturas...',
1121
- * attachmentIds: [file.id],
1685
+ * @example Autenticación con OAuth 2.1 + PKCE
1686
+ * ```typescript
1687
+ * const sdk = new UtiliaSDK({
1688
+ * baseURL: 'https://os.utilia.ai/api',
1689
+ * oauth: {
1690
+ * clientId: 'tu-client-id',
1691
+ * redirectUri: 'https://tu-app.com/callback',
1692
+ * },
1122
1693
  * });
1694
+ *
1695
+ * // Obtener URL de autorización (gestiona PKCE automáticamente)
1696
+ * const authUrl = await sdk.oauth.getAuthorizationUrl();
1697
+ *
1698
+ * // Tras el callback (recupera PKCE automáticamente)
1699
+ * const tokens = await sdk.oauth.handleCallback(code);
1123
1700
  * ```
1124
1701
  */
1125
1702
  constructor(config) {
1126
- const client = new UtiliaClient(config);
1127
- this.errors = new ErrorsService(client);
1128
- this.files = new FilesService(client);
1129
- this.tickets = new TicketsService(client);
1130
- this.users = new UsersService(client);
1131
- this.ai = new AiService(client);
1703
+ this._client = new UtiliaClient(config);
1704
+ this.errors = new ErrorsService(this._client);
1705
+ this.files = new FilesService(this._client);
1706
+ this.tickets = new TicketsService(this._client);
1707
+ this.users = new UsersService(this._client);
1708
+ this.ai = new AiService(this._client);
1709
+ }
1710
+ /**
1711
+ * Servicio OAuth (solo disponible si se configuro oauth en el constructor)
1712
+ */
1713
+ get oauth() {
1714
+ return this._client.oauth;
1132
1715
  }
1133
1716
  };
1134
1717
  export {
1135
1718
  ErrorCode,
1719
+ FileTokenStorage,
1720
+ MemoryTokenStorage,
1721
+ OAuthService,
1136
1722
  SDK_LIMITS,
1137
1723
  UtiliaSDK,
1138
- UtiliaSDKError
1724
+ UtiliaSDKError,
1725
+ base64UrlEncode,
1726
+ generateCodeChallenge,
1727
+ generateCodeVerifier
1139
1728
  };