@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.js CHANGED
@@ -31,14 +31,20 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  ErrorCode: () => ErrorCode,
34
+ FileTokenStorage: () => FileTokenStorage,
35
+ MemoryTokenStorage: () => MemoryTokenStorage,
36
+ OAuthService: () => OAuthService,
34
37
  SDK_LIMITS: () => SDK_LIMITS,
35
38
  UtiliaSDK: () => UtiliaSDK,
36
- UtiliaSDKError: () => UtiliaSDKError
39
+ UtiliaSDKError: () => UtiliaSDKError,
40
+ base64UrlEncode: () => base64UrlEncode,
41
+ generateCodeChallenge: () => generateCodeChallenge,
42
+ generateCodeVerifier: () => generateCodeVerifier
37
43
  });
38
44
  module.exports = __toCommonJS(index_exports);
39
45
 
40
46
  // src/client.ts
41
- var import_axios = __toESM(require("axios"));
47
+ var import_axios2 = __toESM(require("axios"));
42
48
 
43
49
  // src/errors/sdk-error.ts
44
50
  var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
@@ -48,6 +54,7 @@ var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
48
54
  ErrorCode2["RATE_LIMITED"] = "RATE_LIMITED";
49
55
  ErrorCode2["VALIDATION_ERROR"] = "VALIDATION_ERROR";
50
56
  ErrorCode2["NETWORK_ERROR"] = "NETWORK_ERROR";
57
+ ErrorCode2["AUTH_EXPIRED"] = "AUTH_EXPIRED";
51
58
  ErrorCode2["UNKNOWN"] = "UNKNOWN";
52
59
  return ErrorCode2;
53
60
  })(ErrorCode || {});
@@ -98,23 +105,469 @@ var UtiliaSDKError = class _UtiliaSDKError extends Error {
98
105
  }
99
106
  };
100
107
 
108
+ // src/auth/oauth.service.ts
109
+ var import_axios = __toESM(require("axios"));
110
+
111
+ // src/auth/pkce.ts
112
+ function base64UrlEncode(buffer) {
113
+ const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
114
+ let binary = "";
115
+ for (let i = 0; i < bytes.length; i++) {
116
+ binary += String.fromCharCode(bytes[i]);
117
+ }
118
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
119
+ }
120
+ function generateCodeVerifier(length = 64) {
121
+ const validLength = Math.max(43, Math.min(128, length));
122
+ const bytes = new Uint8Array(validLength);
123
+ crypto.getRandomValues(bytes);
124
+ return base64UrlEncode(bytes).slice(0, validLength);
125
+ }
126
+ async function generateCodeChallenge(verifier) {
127
+ const encoder = new TextEncoder();
128
+ const data = encoder.encode(verifier);
129
+ const digest = await crypto.subtle.digest("SHA-256", data);
130
+ return base64UrlEncode(digest);
131
+ }
132
+
133
+ // src/auth/token-storage.ts
134
+ var MemoryTokenStorage = class {
135
+ constructor() {
136
+ this.tokens = null;
137
+ this.pendingState = null;
138
+ }
139
+ async getTokens() {
140
+ return this.tokens;
141
+ }
142
+ async setTokens(tokens) {
143
+ this.tokens = tokens;
144
+ }
145
+ async clearTokens() {
146
+ this.tokens = null;
147
+ }
148
+ async getPendingState() {
149
+ return this.pendingState;
150
+ }
151
+ async setPendingState(state) {
152
+ this.pendingState = state;
153
+ }
154
+ async clearPendingState() {
155
+ this.pendingState = null;
156
+ }
157
+ };
158
+ var FileTokenStorage = class {
159
+ /**
160
+ * @param filePath - Ruta absoluta al archivo donde se guardarán los tokens
161
+ */
162
+ constructor(filePath) {
163
+ this.filePath = filePath;
164
+ }
165
+ async getTokens() {
166
+ try {
167
+ const fs = await import("fs/promises");
168
+ const content = await fs.readFile(this.filePath, "utf-8");
169
+ const data = JSON.parse(content);
170
+ if (data._pendingState) {
171
+ return null;
172
+ }
173
+ return data;
174
+ } catch {
175
+ return null;
176
+ }
177
+ }
178
+ async setTokens(tokens) {
179
+ const fs = await import("fs/promises");
180
+ await fs.writeFile(this.filePath, JSON.stringify(tokens, null, 2), { encoding: "utf-8", mode: 384 });
181
+ }
182
+ async clearTokens() {
183
+ try {
184
+ const fs = await import("fs/promises");
185
+ await fs.unlink(this.filePath);
186
+ } catch {
187
+ }
188
+ }
189
+ get pendingStatePath() {
190
+ return this.filePath.replace(/\.json$/, "") + ".pending.json";
191
+ }
192
+ async getPendingState() {
193
+ try {
194
+ const fs = await import("fs/promises");
195
+ const content = await fs.readFile(this.pendingStatePath, "utf-8");
196
+ return JSON.parse(content);
197
+ } catch {
198
+ return null;
199
+ }
200
+ }
201
+ async setPendingState(state) {
202
+ const fs = await import("fs/promises");
203
+ await fs.writeFile(this.pendingStatePath, JSON.stringify(state, null, 2), { encoding: "utf-8", mode: 384 });
204
+ }
205
+ async clearPendingState() {
206
+ try {
207
+ const fs = await import("fs/promises");
208
+ await fs.unlink(this.pendingStatePath);
209
+ } catch {
210
+ }
211
+ }
212
+ };
213
+
214
+ // src/auth/oauth.service.ts
215
+ var OAuthService = class {
216
+ constructor(baseURL, config) {
217
+ /** Estado PKCE en memoria como fallback cuando el storage no soporta pendingState */
218
+ this._pendingState = null;
219
+ this.baseURL = baseURL.replace(/\/$/, "");
220
+ this.config = config;
221
+ this.storage = config.tokenStorage ?? new MemoryTokenStorage();
222
+ this.http = import_axios.default.create({
223
+ baseURL: this.baseURL,
224
+ timeout: 3e4,
225
+ headers: { "Content-Type": "application/json" }
226
+ });
227
+ }
228
+ /**
229
+ * Genera la URL de autorización OAuth con PKCE y almacena el estado
230
+ * pendiente automáticamente. Usar con handleCallback(code) sin codeVerifier.
231
+ *
232
+ * Este es el método recomendado para la mayoría de los casos.
233
+ *
234
+ * @param options - Opciones adicionales (state personalizado, scopes)
235
+ * @returns URL de autorización lista para redirigir al usuario
236
+ */
237
+ async getAuthorizationUrl(options) {
238
+ const result = await this.getLoginUrl(options);
239
+ const pending = {
240
+ codeVerifier: result.codeVerifier,
241
+ state: result.state
242
+ };
243
+ this._pendingState = pending;
244
+ if (this.storage.setPendingState) {
245
+ await this.storage.setPendingState(pending);
246
+ }
247
+ return result.url;
248
+ }
249
+ /**
250
+ * Genera la URL de inicio de sesión OAuth con PKCE.
251
+ *
252
+ * Método de control manual: devuelve codeVerifier y state que el desarrollador
253
+ * debe gestionar. Para un flujo automático, usar getAuthorizationUrl() en su lugar.
254
+ *
255
+ * @param options - Opciones adicionales (state personalizado, scopes)
256
+ * @returns URL de autorización, code_verifier y state
257
+ */
258
+ async getLoginUrl(options) {
259
+ const codeVerifier = generateCodeVerifier();
260
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
261
+ const state = options?.state ?? Array.from(crypto.getRandomValues(new Uint8Array(24)), (b) => b.toString(16).padStart(2, "0")).join("");
262
+ const scopes = options?.scopes ?? this.config.scopes ?? ["openid", "profile", "email"];
263
+ const params = new URLSearchParams({
264
+ response_type: "code",
265
+ client_id: this.config.clientId,
266
+ redirect_uri: this.config.redirectUri,
267
+ scope: scopes.join(" "),
268
+ state,
269
+ code_challenge: codeChallenge,
270
+ code_challenge_method: "S256"
271
+ });
272
+ return {
273
+ url: `${this.baseURL}/oauth/authorize?${params.toString()}`,
274
+ codeVerifier,
275
+ state
276
+ };
277
+ }
278
+ /**
279
+ * Abre una ventana emergente para inicio de sesión OAuth y devuelve tokens al completarse.
280
+ *
281
+ * @param options - Opciones de configuración de la ventana emergente
282
+ * @returns Tokens OAuth
283
+ * @throws Error si la ventana es bloqueada, cerrada o excede el tiempo límite
284
+ */
285
+ async loginWithPopup(options) {
286
+ if (typeof window === "undefined") {
287
+ throw new Error("loginWithPopup solo est\xE1 disponible en entornos de navegador.");
288
+ }
289
+ const { url, codeVerifier, state } = await this.getLoginUrl({
290
+ state: options?.state,
291
+ scopes: options?.scopes
292
+ });
293
+ const width = options?.width ?? 500;
294
+ const height = options?.height ?? 700;
295
+ const timeout = options?.timeout ?? 3e5;
296
+ const left = Math.max(0, Math.round(window.screenX + (window.outerWidth - width) / 2));
297
+ const top = Math.max(0, Math.round(window.screenY + (window.outerHeight - height) / 2));
298
+ const features = `width=${width},height=${height},left=${left},top=${top},scrollbars=yes,resizable=yes,status=yes`;
299
+ const popup = window.open(url, "utilia-oauth-popup", features);
300
+ if (!popup || popup.closed) {
301
+ throw new Error(
302
+ "No se pudo abrir la ventana de autorizaci\xF3n. Verifica que los popups no est\xE9n bloqueados."
303
+ );
304
+ }
305
+ return new Promise((resolve, reject) => {
306
+ let timeoutId;
307
+ let pollId;
308
+ const cleanup = () => {
309
+ clearTimeout(timeoutId);
310
+ clearInterval(pollId);
311
+ window.removeEventListener("message", onMessage);
312
+ };
313
+ const onMessage = async (event) => {
314
+ if (event.origin !== window.location.origin) return;
315
+ const data = event.data;
316
+ if (!data || typeof data !== "object") return;
317
+ if (data.type === "oauth-mcp-success" && data.state === state) {
318
+ cleanup();
319
+ try {
320
+ if (popup && !popup.closed) popup.close();
321
+ } catch {
322
+ }
323
+ try {
324
+ const tokens = await this.handleCallback(data.code, codeVerifier);
325
+ resolve(tokens);
326
+ } catch (err) {
327
+ reject(err);
328
+ }
329
+ } else if (data.type === "oauth-mcp-error" && data.state === state) {
330
+ cleanup();
331
+ try {
332
+ if (popup && !popup.closed) popup.close();
333
+ } catch {
334
+ }
335
+ reject(new Error(data.error_description || data.error || "Autorizaci\xF3n denegada"));
336
+ }
337
+ };
338
+ window.addEventListener("message", onMessage);
339
+ pollId = setInterval(() => {
340
+ if (popup.closed) {
341
+ cleanup();
342
+ reject(new Error("La ventana de autorizaci\xF3n fue cerrada antes de completar el proceso."));
343
+ }
344
+ }, 500);
345
+ timeoutId = setTimeout(() => {
346
+ cleanup();
347
+ try {
348
+ if (popup && !popup.closed) popup.close();
349
+ } catch {
350
+ }
351
+ reject(new Error("El proceso de autorizaci\xF3n excedi\xF3 el tiempo l\xEDmite."));
352
+ }, timeout);
353
+ });
354
+ }
355
+ /**
356
+ * Intercambia el código de autorización por tokens.
357
+ *
358
+ * Si se llama sin codeVerifier, recupera automáticamente el estado PKCE
359
+ * almacenado por getAuthorizationUrl() y valida el state CSRF.
360
+ *
361
+ * @param code - Código de autorización recibido en el callback
362
+ * @param codeVerifier - Verificador PKCE (opcional si se usó getAuthorizationUrl)
363
+ * @param callbackState - Valor de state recibido en el callback (para validación CSRF automática)
364
+ * @returns Tokens OAuth
365
+ */
366
+ async handleCallback(code, codeVerifier, callbackState) {
367
+ let verifier = codeVerifier;
368
+ if (!verifier) {
369
+ const pending = await this.getPendingState();
370
+ if (!pending) {
371
+ throw new Error(
372
+ "No se encontr\xF3 el estado PKCE pendiente. Usa getAuthorizationUrl() antes de handleCallback(), o proporciona el codeVerifier manualmente."
373
+ );
374
+ }
375
+ verifier = pending.codeVerifier;
376
+ if (callbackState && callbackState !== pending.state) {
377
+ await this.clearPendingState();
378
+ throw new Error(
379
+ `El valor de state no coincide. Posible ataque CSRF. Esperado: ${pending.state}, recibido: ${callbackState}`
380
+ );
381
+ }
382
+ await this.clearPendingState();
383
+ }
384
+ const body = {
385
+ grant_type: "authorization_code",
386
+ code,
387
+ redirect_uri: this.config.redirectUri,
388
+ code_verifier: verifier,
389
+ client_id: this.config.clientId
390
+ };
391
+ if (this.config.clientSecret) {
392
+ body.client_secret = this.config.clientSecret;
393
+ }
394
+ const response = await this.http.post("/oauth/token", body);
395
+ const data = response.data;
396
+ const tokens = {
397
+ accessToken: data.access_token,
398
+ refreshToken: data.refresh_token,
399
+ expiresIn: data.expires_in,
400
+ expiresAt: Date.now() + data.expires_in * 1e3,
401
+ tokenType: data.token_type || "Bearer",
402
+ scope: data.scope,
403
+ idToken: data.id_token
404
+ };
405
+ await this.storage.setTokens(tokens);
406
+ return tokens;
407
+ }
408
+ /**
409
+ * Refresca el token de acceso usando el refresh_token
410
+ *
411
+ * @returns Nuevos tokens OAuth
412
+ * @throws Error si no hay refresh_token disponible
413
+ */
414
+ async refreshToken() {
415
+ const current = await this.storage.getTokens();
416
+ if (!current?.refreshToken) {
417
+ throw new Error("No hay refresh token disponible. El usuario debe volver a autenticarse.");
418
+ }
419
+ const body = {
420
+ grant_type: "refresh_token",
421
+ refresh_token: current.refreshToken,
422
+ client_id: this.config.clientId
423
+ };
424
+ if (this.config.clientSecret) {
425
+ body.client_secret = this.config.clientSecret;
426
+ }
427
+ const response = await this.http.post("/oauth/token", body);
428
+ const data = response.data;
429
+ const tokens = {
430
+ accessToken: data.access_token,
431
+ refreshToken: data.refresh_token ?? current.refreshToken,
432
+ expiresIn: data.expires_in,
433
+ expiresAt: Date.now() + data.expires_in * 1e3,
434
+ tokenType: data.token_type || "Bearer",
435
+ scope: data.scope,
436
+ idToken: data.id_token
437
+ };
438
+ await this.storage.setTokens(tokens);
439
+ return tokens;
440
+ }
441
+ /**
442
+ * Obtiene información del usuario autenticado
443
+ *
444
+ * @returns Datos del usuario desde /oauth/userinfo
445
+ */
446
+ async getUserInfo() {
447
+ const accessToken = await this.getAccessToken();
448
+ const response = await this.http.get("/oauth/userinfo", {
449
+ headers: { Authorization: `Bearer ${accessToken}` }
450
+ });
451
+ return response.data;
452
+ }
453
+ /**
454
+ * Revoca el token actual
455
+ *
456
+ * @param tokenType - Tipo de token a revocar ('access_token' | 'refresh_token').
457
+ * Por defecto revoca el access_token.
458
+ */
459
+ async revokeToken(tokenType) {
460
+ const current = await this.storage.getTokens();
461
+ if (!current) return;
462
+ const token = tokenType === "refresh_token" ? current.refreshToken : current.accessToken;
463
+ if (!token) return;
464
+ const body = {
465
+ token,
466
+ client_id: this.config.clientId
467
+ };
468
+ if (tokenType) {
469
+ body.token_type_hint = tokenType;
470
+ }
471
+ if (this.config.clientSecret) {
472
+ body.client_secret = this.config.clientSecret;
473
+ }
474
+ try {
475
+ await this.http.post("/oauth/revoke", body);
476
+ } catch {
477
+ }
478
+ await this.storage.clearTokens();
479
+ }
480
+ /**
481
+ * Obtiene un access token válido, refrescándolo automáticamente si ha expirado
482
+ *
483
+ * @returns Access token válido
484
+ * @throws Error si no hay tokens disponibles
485
+ */
486
+ async getAccessToken() {
487
+ const tokens = await this.storage.getTokens();
488
+ if (!tokens) {
489
+ throw new Error("No hay tokens OAuth. El usuario debe iniciar sesi\xF3n primero.");
490
+ }
491
+ if (tokens.expiresAt < Date.now() + 3e4) {
492
+ const refreshed = await this.refreshToken();
493
+ return refreshed.accessToken;
494
+ }
495
+ return tokens.accessToken;
496
+ }
497
+ /**
498
+ * Verifica si hay una sesión OAuth activa con tokens válidos.
499
+ *
500
+ * @returns true si hay tokens almacenados y el access token no ha expirado
501
+ * (o si hay un refresh token disponible para renovarlo)
502
+ */
503
+ async isAuthenticated() {
504
+ const tokens = await this.storage.getTokens();
505
+ if (!tokens) return false;
506
+ if (tokens.expiresAt > Date.now() + 3e4) return true;
507
+ return !!tokens.refreshToken;
508
+ }
509
+ /**
510
+ * Cierra la sesión OAuth: revoca los tokens y limpia el almacenamiento.
511
+ */
512
+ async logout() {
513
+ await this.revokeToken();
514
+ }
515
+ // -- Helpers internos para estado PKCE pendiente --
516
+ async getPendingState() {
517
+ if (this.storage.getPendingState) {
518
+ const stored = await this.storage.getPendingState();
519
+ if (stored) return stored;
520
+ }
521
+ return this._pendingState;
522
+ }
523
+ async clearPendingState() {
524
+ this._pendingState = null;
525
+ if (this.storage.clearPendingState) {
526
+ await this.storage.clearPendingState();
527
+ }
528
+ }
529
+ };
530
+
101
531
  // src/client.ts
102
532
  var UtiliaClient = class {
103
533
  constructor(config) {
534
+ /** Promesa compartida para evitar refreshes concurrentes */
535
+ this._refreshPromise = null;
536
+ if (!config.apiKey && !config.oauth) {
537
+ throw new UtiliaSDKError(
538
+ "VALIDATION_ERROR" /* VALIDATION_ERROR */,
539
+ "Debes proporcionar apiKey o configuraci\xF3n oauth"
540
+ );
541
+ }
542
+ if (config.apiKey && config.oauth) {
543
+ throw new UtiliaSDKError(
544
+ "VALIDATION_ERROR" /* VALIDATION_ERROR */,
545
+ "No puedes proporcionar apiKey y oauth a la vez. Usa uno u otro."
546
+ );
547
+ }
104
548
  this.config = {
105
549
  timeout: 3e4,
106
550
  retryAttempts: 3,
107
551
  debug: false,
108
552
  ...config
109
553
  };
110
- this.axios = import_axios.default.create({
554
+ const headers = {
555
+ "Content-Type": "application/json"
556
+ };
557
+ if (this.config.apiKey) {
558
+ headers["X-Api-Key"] = this.config.apiKey;
559
+ }
560
+ this.axios = import_axios2.default.create({
111
561
  baseURL: this.config.baseURL,
112
562
  timeout: this.config.timeout,
113
- headers: {
114
- "Content-Type": "application/json",
115
- "X-Api-Key": this.config.apiKey
116
- }
563
+ headers
117
564
  });
565
+ if (this.config.oauth) {
566
+ this._oauthService = new OAuthService(this.config.baseURL, this.config.oauth);
567
+ if (this.config.oauth.tokenStorage) {
568
+ }
569
+ this.setupOAuthInterceptors();
570
+ }
118
571
  this.axios.interceptors.request.use((config2) => {
119
572
  const requestId = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : this.generateSimpleId();
120
573
  config2.headers["x-request-id"] = requestId;
@@ -123,20 +576,71 @@ var UtiliaClient = class {
123
576
  });
124
577
  this.setupInterceptors();
125
578
  }
579
+ /**
580
+ * Acceso al servicio OAuth (solo disponible en modo OAuth)
581
+ */
582
+ get oauth() {
583
+ if (!this._oauthService) {
584
+ throw new UtiliaSDKError(
585
+ "VALIDATION_ERROR" /* VALIDATION_ERROR */,
586
+ "El servicio OAuth no est\xE1 disponible. Configura oauth en el constructor del SDK."
587
+ );
588
+ }
589
+ return this._oauthService;
590
+ }
591
+ /**
592
+ * Configura interceptores para autenticación OAuth
593
+ */
594
+ setupOAuthInterceptors() {
595
+ this.axios.interceptors.request.use(async (config) => {
596
+ if (this._oauthService) {
597
+ try {
598
+ const accessToken = await this._oauthService.getAccessToken();
599
+ config.headers["Authorization"] = `Bearer ${accessToken}`;
600
+ } catch {
601
+ }
602
+ }
603
+ return config;
604
+ });
605
+ this.axios.interceptors.response.use(
606
+ (response) => response,
607
+ async (error) => {
608
+ const originalRequest = error.config;
609
+ if (error.response?.status === 401 && this._oauthService && originalRequest && !originalRequest._retried) {
610
+ originalRequest._retried = true;
611
+ if (!this._refreshPromise) {
612
+ this._refreshPromise = this._oauthService.refreshToken().then(() => {
613
+ }).finally(() => {
614
+ this._refreshPromise = null;
615
+ });
616
+ }
617
+ try {
618
+ await this._refreshPromise;
619
+ const newToken = await this._oauthService.getAccessToken();
620
+ originalRequest.headers["Authorization"] = `Bearer ${newToken}`;
621
+ return this.axios(originalRequest);
622
+ } catch {
623
+ throw error;
624
+ }
625
+ }
626
+ throw error;
627
+ }
628
+ );
629
+ }
126
630
  /**
127
631
  * Configura los interceptores de request y response
128
632
  */
129
633
  setupInterceptors() {
130
634
  if (this.config.debug) {
131
635
  this.axios.interceptors.request.use((request) => {
132
- console.log("[UtiliaSDK] Request:", request.method?.toUpperCase(), request.url);
636
+ console.log("[UtiliaSDK] Petici\xF3n:", request.method?.toUpperCase(), request.url);
133
637
  return request;
134
638
  });
135
639
  }
136
640
  if (this.config.debug) {
137
641
  this.axios.interceptors.response.use(
138
642
  (response) => {
139
- console.log("[UtiliaSDK] Response:", response.status, response.config.url);
643
+ console.log("[UtiliaSDK] Respuesta:", response.status, response.config.url);
140
644
  return response;
141
645
  }
142
646
  );
@@ -153,15 +657,15 @@ var UtiliaClient = class {
153
657
  const errorOptions = { requestId, statusCode, errorCode };
154
658
  if (!error.response) {
155
659
  if (error.code === "ECONNABORTED") {
156
- return new UtiliaSDKError("NETWORK_ERROR" /* NETWORK_ERROR */, "Timeout: la solicitud excedio el tiempo limite", errorOptions);
660
+ return new UtiliaSDKError("NETWORK_ERROR" /* NETWORK_ERROR */, "Timeout: la solicitud excedi\xF3 el tiempo l\xEDmite", errorOptions);
157
661
  }
158
- return new UtiliaSDKError("NETWORK_ERROR" /* NETWORK_ERROR */, "Error de conexion: no se pudo conectar con el servidor", errorOptions);
662
+ return new UtiliaSDKError("NETWORK_ERROR" /* NETWORK_ERROR */, "Error de conexi\xF3n: no se pudo conectar con el servidor", errorOptions);
159
663
  }
160
664
  const status = error.response.status;
161
665
  const message = data?.message || data?.error || error.message;
162
666
  switch (status) {
163
667
  case 401:
164
- return new UtiliaSDKError("UNAUTHORIZED" /* UNAUTHORIZED */, message || "API Key invalida o faltante", errorOptions);
668
+ return new UtiliaSDKError("UNAUTHORIZED" /* UNAUTHORIZED */, message || "API Key inv\xE1lida o faltante", errorOptions);
165
669
  case 403:
166
670
  if (message?.toLowerCase().includes("rate limit")) {
167
671
  return new UtiliaSDKError("RATE_LIMITED" /* RATE_LIMITED */, "Rate limit excedido. Intenta de nuevo mas tarde.", errorOptions);
@@ -174,7 +678,7 @@ var UtiliaClient = class {
174
678
  return new UtiliaSDKError("NOT_FOUND" /* NOT_FOUND */, message || "Recurso no encontrado", errorOptions);
175
679
  case 400:
176
680
  case 422:
177
- return new UtiliaSDKError("VALIDATION_ERROR" /* VALIDATION_ERROR */, message || "Error de validacion en los datos enviados", errorOptions);
681
+ return new UtiliaSDKError("VALIDATION_ERROR" /* VALIDATION_ERROR */, message || "Error de validaci\xF3n en los datos enviados", errorOptions);
178
682
  case 429:
179
683
  return new UtiliaSDKError("RATE_LIMITED" /* RATE_LIMITED */, message || "Rate limit excedido. Intenta de nuevo mas tarde.", errorOptions);
180
684
  case 500:
@@ -187,10 +691,10 @@ var UtiliaClient = class {
187
691
  }
188
692
  }
189
693
  /**
190
- * Determina si un error es recuperable y se debe reintentar
694
+ * Determina si un error es recuperable y se debe reintentar.
191
695
  */
192
696
  isRetryableError(error) {
193
- if (error instanceof import_axios.AxiosError) {
697
+ if (error instanceof import_axios2.AxiosError) {
194
698
  if (!error.response) {
195
699
  const retryCodes = ["ECONNABORTED", "ECONNREFUSED", "ECONNRESET", "ETIMEDOUT", "ENETUNREACH", "ERR_NETWORK"];
196
700
  return retryCodes.includes(error.code || "");
@@ -201,7 +705,7 @@ var UtiliaClient = class {
201
705
  return false;
202
706
  }
203
707
  /**
204
- * Ejecuta una funcion con reintentos y backoff exponencial
708
+ * Ejecuta una función con reintentos y backoff exponencial
205
709
  */
206
710
  async executeWithRetry(fn) {
207
711
  let lastError;
@@ -211,7 +715,7 @@ var UtiliaClient = class {
211
715
  } catch (error) {
212
716
  lastError = error;
213
717
  if (!this.isRetryableError(error)) {
214
- if (error instanceof import_axios.AxiosError) {
718
+ if (error instanceof import_axios2.AxiosError) {
215
719
  throw this.toSDKError(error);
216
720
  }
217
721
  throw error;
@@ -219,19 +723,19 @@ var UtiliaClient = class {
219
723
  if (attempt < this.config.retryAttempts - 1) {
220
724
  const delay = 500 * Math.pow(2, attempt);
221
725
  if (this.config.debug) {
222
- console.log(`[UtiliaSDK] Retry attempt ${attempt + 1}/${this.config.retryAttempts - 1}, waiting ${delay}ms`);
726
+ console.log(`[UtiliaSDK] Reintento ${attempt + 1}/${this.config.retryAttempts - 1}, esperando ${delay}ms`);
223
727
  }
224
728
  await this.sleep(delay);
225
729
  }
226
730
  }
227
731
  }
228
- if (lastError instanceof import_axios.AxiosError) {
732
+ if (lastError instanceof import_axios2.AxiosError) {
229
733
  throw this.toSDKError(lastError);
230
734
  }
231
735
  throw lastError;
232
736
  }
233
737
  /**
234
- * Genera un ID simple como fallback cuando crypto.randomUUID no esta disponible
738
+ * Genera un ID simple como fallback cuando crypto.randomUUID no está disponible
235
739
  */
236
740
  generateSimpleId() {
237
741
  return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
@@ -243,19 +747,44 @@ var UtiliaClient = class {
243
747
  return new Promise((resolve) => setTimeout(resolve, ms));
244
748
  }
245
749
  /**
246
- * Construye una URL completa para conexiones SSE con la API key como query param.
247
- * Util para EventSource que no soporta headers personalizados.
750
+ * Construye una URL completa para conexiones SSE con credenciales como query param.
751
+ * Necesario porque EventSource no soporta headers personalizados.
248
752
  *
249
753
  * @param path - Ruta relativa de la API (ej: /external/v1/tickets/ai-suggest/123/stream)
250
- * @returns URL completa con apiKey como parametro de consulta
754
+ * @returns URL completa con credenciales como parametro de consulta
251
755
  */
252
756
  buildSseUrl(path) {
253
757
  const base = this.config.baseURL.replace(/\/$/, "");
254
758
  const separator = path.includes("?") ? "&" : "?";
255
- return `${base}${path}${separator}apiKey=${this.config.apiKey}`;
759
+ if (this.config.apiKey) {
760
+ return `${base}${path}${separator}apiKey=${this.config.apiKey}`;
761
+ }
762
+ return `${base}${path}`;
763
+ }
764
+ /**
765
+ * Construye una URL completa para conexiones SSE, incluyendo token OAuth si está disponible.
766
+ * Versión asíncrona que obtiene el access token actual.
767
+ *
768
+ * @param path - Ruta relativa de la API
769
+ * @returns URL completa con credenciales como parametro de consulta
770
+ */
771
+ async buildSseUrlAsync(path) {
772
+ const base = this.config.baseURL.replace(/\/$/, "");
773
+ const separator = path.includes("?") ? "&" : "?";
774
+ if (this.config.apiKey) {
775
+ return `${base}${path}${separator}apiKey=${this.config.apiKey}`;
776
+ }
777
+ if (this._oauthService) {
778
+ try {
779
+ const accessToken = await this._oauthService.getAccessToken();
780
+ return `${base}${path}${separator}access_token=${accessToken}`;
781
+ } catch {
782
+ }
783
+ }
784
+ return `${base}${path}`;
256
785
  }
257
786
  /**
258
- * Realiza una peticion GET
787
+ * Realiza una petición GET
259
788
  */
260
789
  async get(url, config) {
261
790
  return this.executeWithRetry(async () => {
@@ -635,6 +1164,54 @@ var TicketsService = class {
635
1164
  { params: { userId } }
636
1165
  );
637
1166
  }
1167
+ /**
1168
+ * Suscribirse a actualizaciones de tickets en tiempo real via SSE.
1169
+ * Recibe eventos cuando un agente responde, cambia el estado, resuelve o cierra un ticket.
1170
+ *
1171
+ * Requiere un entorno con EventSource nativo (navegador) o un polyfill como 'eventsource'.
1172
+ *
1173
+ * @param userId - ID externo del usuario (opcional, filtra eventos por usuario)
1174
+ * @param options - Callbacks para eventos y errores
1175
+ * @returns Objeto con metodo close() para cerrar la conexion
1176
+ *
1177
+ * @example
1178
+ * ```typescript
1179
+ * const stream = sdk.tickets.streamUpdates('user-123', {
1180
+ * onTicketUpdated: (event) => {
1181
+ * console.log(`Ticket ${event.ticketKey} actualizado: ${event.type}`);
1182
+ * // Refrescar la UI del ticket
1183
+ * },
1184
+ * });
1185
+ *
1186
+ * // Para cerrar la conexion:
1187
+ * stream.close();
1188
+ * ```
1189
+ */
1190
+ streamUpdates(userId, options) {
1191
+ if (typeof EventSource === "undefined") {
1192
+ throw new Error(
1193
+ 'EventSource no esta disponible en este entorno. Para Node.js, instala un polyfill como "eventsource" y asignalo a globalThis.EventSource.'
1194
+ );
1195
+ }
1196
+ const queryParams = userId ? `?userId=${encodeURIComponent(userId)}` : "";
1197
+ const sseUrl = this.client.buildSseUrl(`${this.basePath}/events${queryParams}`);
1198
+ const eventSource = new EventSource(sseUrl);
1199
+ eventSource.onmessage = (event) => {
1200
+ try {
1201
+ const data = JSON.parse(event.data);
1202
+ if (data.event === "ticket-updated" && options?.onTicketUpdated) {
1203
+ options.onTicketUpdated(data);
1204
+ }
1205
+ } catch {
1206
+ }
1207
+ };
1208
+ eventSource.onerror = () => {
1209
+ options?.onError?.(new Error("Error en la conexion SSE"));
1210
+ };
1211
+ return {
1212
+ close: () => eventSource.close()
1213
+ };
1214
+ }
638
1215
  /**
639
1216
  * Cerrar un ticket
640
1217
  * Solo el usuario que creo el ticket puede cerrarlo
@@ -1140,40 +1717,58 @@ var UtiliaSDK = class {
1140
1717
  /**
1141
1718
  * Crea una nueva instancia del SDK
1142
1719
  *
1143
- * @param config - Configuracion del SDK
1720
+ * @param config - Configuración del SDK
1144
1721
  *
1145
- * @example
1722
+ * @example Autenticación con API Key
1146
1723
  * ```typescript
1147
1724
  * const sdk = new UtiliaSDK({
1148
1725
  * baseURL: 'https://os.utilia.ai/api',
1149
1726
  * apiKey: 'tu-api-key',
1150
- * timeout: 30000, // opcional
1151
- * debug: true, // opcional, habilita logs
1152
1727
  * });
1728
+ * ```
1153
1729
  *
1154
- * // Subir archivo y crear ticket con adjunto
1155
- * const file = await sdk.files.upload(inputFile);
1156
- * const ticket = await sdk.tickets.create({
1157
- * user: { externalId: 'user-123' },
1158
- * title: 'Problema con facturacion',
1159
- * description: 'No puedo ver mis facturas...',
1160
- * attachmentIds: [file.id],
1730
+ * @example Autenticación con OAuth 2.1 + PKCE
1731
+ * ```typescript
1732
+ * const sdk = new UtiliaSDK({
1733
+ * baseURL: 'https://os.utilia.ai/api',
1734
+ * oauth: {
1735
+ * clientId: 'tu-client-id',
1736
+ * redirectUri: 'https://tu-app.com/callback',
1737
+ * },
1161
1738
  * });
1739
+ *
1740
+ * // Obtener URL de autorización (gestiona PKCE automáticamente)
1741
+ * const authUrl = await sdk.oauth.getAuthorizationUrl();
1742
+ *
1743
+ * // Tras el callback (recupera PKCE automáticamente)
1744
+ * const tokens = await sdk.oauth.handleCallback(code);
1162
1745
  * ```
1163
1746
  */
1164
1747
  constructor(config) {
1165
- const client = new UtiliaClient(config);
1166
- this.errors = new ErrorsService(client);
1167
- this.files = new FilesService(client);
1168
- this.tickets = new TicketsService(client);
1169
- this.users = new UsersService(client);
1170
- this.ai = new AiService(client);
1748
+ this._client = new UtiliaClient(config);
1749
+ this.errors = new ErrorsService(this._client);
1750
+ this.files = new FilesService(this._client);
1751
+ this.tickets = new TicketsService(this._client);
1752
+ this.users = new UsersService(this._client);
1753
+ this.ai = new AiService(this._client);
1754
+ }
1755
+ /**
1756
+ * Servicio OAuth (solo disponible si se configuro oauth en el constructor)
1757
+ */
1758
+ get oauth() {
1759
+ return this._client.oauth;
1171
1760
  }
1172
1761
  };
1173
1762
  // Annotate the CommonJS export names for ESM import in node:
1174
1763
  0 && (module.exports = {
1175
1764
  ErrorCode,
1765
+ FileTokenStorage,
1766
+ MemoryTokenStorage,
1767
+ OAuthService,
1176
1768
  SDK_LIMITS,
1177
1769
  UtiliaSDK,
1178
- UtiliaSDKError
1770
+ UtiliaSDKError,
1771
+ base64UrlEncode,
1772
+ generateCodeChallenge,
1773
+ generateCodeVerifier
1179
1774
  });